mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Merge of features tied to 'Piri Piri' funding goal'
This commit is contained in:
parent
03d065ca20
commit
1bee037713
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 %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script src="{{ 'assets/javascripts/custom.a678ee80.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/custom.98e0b405.min.js' | url }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
29
material/assets/javascripts/bundle.202856ae.min.js
vendored
Normal file
29
material/assets/javascripts/bundle.202856ae.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
material/assets/javascripts/bundle.202856ae.min.js.map
Normal file
8
material/assets/javascripts/bundle.202856ae.min.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/main.08fbbb04.min.css
vendored
Normal file
1
material/assets/stylesheets/main.08fbbb04.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/main.08fbbb04.min.css.map
Normal file
1
material/assets/stylesheets/main.08fbbb04.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
@ -27,6 +27,10 @@
|
|||||||
{% if page.next_page %}
|
{% if page.next_page %}
|
||||||
<link rel="next" href="{{ page.next_page.url | url }}">
|
<link rel="next" href="{{ page.next_page.url | url }}">
|
||||||
{% endif %}
|
{% 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 }}">
|
<link rel="icon" href="{{ config.theme.favicon | url }}">
|
||||||
<meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-9.1.18">
|
<meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-9.1.18">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -40,7 +44,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block styles %}
|
{% block styles %}
|
||||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.26e3688c.min.css' | url }}">
|
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.08fbbb04.min.css' | url }}">
|
||||||
{% if config.theme.palette %}
|
{% if config.theme.palette %}
|
||||||
{% set palette = config.theme.palette %}
|
{% set palette = config.theme.palette %}
|
||||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.ecc896b0.min.css' | url }}">
|
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.ecc896b0.min.css' | url }}">
|
||||||
@ -212,7 +216,7 @@
|
|||||||
"base": base_url,
|
"base": base_url,
|
||||||
"features": features,
|
"features": features,
|
||||||
"translations": {},
|
"translations": {},
|
||||||
"search": "assets/javascripts/workers/search.74e28a9f.min.js" | url
|
"search": "assets/javascripts/workers/search.780af0f4.min.js" | url
|
||||||
} -%}
|
} -%}
|
||||||
{%- if config.extra.version -%}
|
{%- if config.extra.version -%}
|
||||||
{%- set _ = app.update({ "version": config.extra.version }) -%}
|
{%- set _ = app.update({ "version": config.extra.version }) -%}
|
||||||
@ -240,7 +244,7 @@
|
|||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ 'assets/javascripts/bundle.220ee61c.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/bundle.202856ae.min.js' | url }}"></script>
|
||||||
{% for path in config.extra_javascript %}
|
{% for path in config.extra_javascript %}
|
||||||
{% if path.endswith(".mjs") %}
|
{% if path.endswith(".mjs") %}
|
||||||
<script type="module" src="{{ path | url }}"></script>
|
<script type="module" src="{{ path | url }}"></script>
|
||||||
|
16
material/blog-archive.html
Normal file
16
material/blog-archive.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{#-
|
||||||
|
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 %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
16
material/blog-category.html
Normal file
16
material/blog-category.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{#-
|
||||||
|
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 %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
98
material/blog-post.html
Normal file
98
material/blog-post.html
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{#-
|
||||||
|
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.meta.date }}" class="md-ellipsis">
|
||||||
|
{{- page.meta.date_format -}}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% 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.meta.readtime %}
|
||||||
|
{% set time = page.meta.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 %}
|
19
material/blog.html
Normal file
19
material/blog.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{#-
|
||||||
|
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 %}
|
||||||
|
{% block pagination %}
|
||||||
|
{% include "partials/pagination.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -3,4 +3,4 @@
|
|||||||
-#}
|
-#}
|
||||||
{% import "partials/languages/" ~ config.theme.language ~ ".html" as lang %}
|
{% import "partials/languages/" ~ config.theme.language ~ ".html" as lang %}
|
||||||
{% import "partials/languages/en.html" as fallback %}
|
{% 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 %}
|
||||||
|
@ -26,9 +26,8 @@
|
|||||||
"header": "頁首",
|
"header": "頁首",
|
||||||
"meta.comments": "評論",
|
"meta.comments": "評論",
|
||||||
"meta.source": "來源",
|
"meta.source": "來源",
|
||||||
"search.config.lang": "ja",
|
|
||||||
"search.config.pipeline": "stemmer",
|
"search.config.pipeline": "stemmer",
|
||||||
"search.config.separator": "[\\s\\-,。]+",
|
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||||
"nav": "導航",
|
"nav": "導航",
|
||||||
"readtime.one": "需要 1 分鐘閲讀",
|
"readtime.one": "需要 1 分鐘閲讀",
|
||||||
"readtime.other": "需要 # 分鐘閲讀",
|
"readtime.other": "需要 # 分鐘閲讀",
|
||||||
|
@ -32,9 +32,8 @@
|
|||||||
"rss.created": "RSS 訂閱",
|
"rss.created": "RSS 訂閱",
|
||||||
"rss.updated": "RSS 訂閱內容已更新",
|
"rss.updated": "RSS 訂閱內容已更新",
|
||||||
"search": "搜尋",
|
"search": "搜尋",
|
||||||
"search.config.lang": "ja",
|
|
||||||
"search.config.pipeline": "stemmer",
|
"search.config.pipeline": "stemmer",
|
||||||
"search.config.separator": "[\\s\\- 、。,.?;]+",
|
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||||
"search.placeholder": "搜尋",
|
"search.placeholder": "搜尋",
|
||||||
"search.share": "分享",
|
"search.share": "分享",
|
||||||
"search.reset": "清除",
|
"search.reset": "清除",
|
||||||
|
@ -32,9 +32,8 @@
|
|||||||
"rss.created": "RSS 订阅",
|
"rss.created": "RSS 订阅",
|
||||||
"rss.updated": "已更新内容的 RSS 订阅",
|
"rss.updated": "已更新内容的 RSS 订阅",
|
||||||
"search": "查找",
|
"search": "查找",
|
||||||
"search.config.lang": "ja",
|
|
||||||
"search.config.pipeline": "stemmer",
|
"search.config.pipeline": "stemmer",
|
||||||
"search.config.separator": "[\\s\\-,。]+",
|
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||||
"search.placeholder": "搜索",
|
"search.placeholder": "搜索",
|
||||||
"search.share": "分享",
|
"search.share": "分享",
|
||||||
"search.reset": "清空当前内容",
|
"search.reset": "清空当前内容",
|
||||||
|
@ -1,63 +1,105 @@
|
|||||||
{#-
|
{#-
|
||||||
This file was automatically generated - do not edit
|
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">
|
||||||
|
{{ nav_item.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(first, ref) }}
|
||||||
|
{% if nav_item.children | length > 1 %}
|
||||||
|
<span class="md-nav__icon md-icon"></span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
{% macro render(nav_item, path, level) %}
|
{% macro render(nav_item, path, level) %}
|
||||||
{% set class = "md-nav__item" %}
|
{% set class = "md-nav__item" %}
|
||||||
{% if nav_item.active %}
|
{% if nav_item.active %}
|
||||||
{% set class = class ~ " md-nav__item--active" %}
|
{% set class = class ~ " md-nav__item--active" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if nav_item.children %}
|
{% 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 + (
|
{% if "navigation.sections" in features and level == 1 + (
|
||||||
"navigation.tabs" in features
|
"navigation.tabs" in features
|
||||||
) %}
|
) %}
|
||||||
{% set class = class ~ " md-nav__item--section" %}
|
{% 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 %}
|
{% endif %}
|
||||||
<li class="{{ class }} md-nav__item--nested">
|
<li class="{{ class }} md-nav__item--nested">
|
||||||
{% set expanded = "navigation.expand" in features %}
|
{% if not prune %}
|
||||||
{% set active = nav_item.active or expanded %}
|
{% set expanded = "navigation.expand" in features %}
|
||||||
{% set checked = "checked" if nav_item.active %}
|
{% set active = nav_item.active or expanded %}
|
||||||
{% if expanded and not checked %}
|
{% set checked = "checked" if nav_item.active %}
|
||||||
{% set indeterminate = "md-toggle--indeterminate" %}
|
{% if expanded and not checked %}
|
||||||
{% endif %}
|
{% set indeterminate = "md-toggle--indeterminate" %}
|
||||||
<input class="md-nav__toggle md-toggle {{ indeterminate }}" type="checkbox" id="{{ path }}" {{ checked }}>
|
{% endif %}
|
||||||
{% set indexes = [] %}
|
<input class="md-nav__toggle md-toggle {{ indeterminate }}" type="checkbox" id="{{ path }}" {{ checked }}>
|
||||||
{% if "navigation.indexes" in features %}
|
{% if not indexes %}
|
||||||
{% for nav_item in nav_item.children %}
|
<label class="md-nav__link" for="{{ path }}" id="{{ path }}_label" tabindex="0">
|
||||||
{% if nav_item.is_index and not index is defined %}
|
{{ render_content(nav_item) }}
|
||||||
{% set _ = indexes.append(nav_item) %}
|
<span class="md-nav__icon md-icon"></span>
|
||||||
{% endif %}
|
</label>
|
||||||
{% endfor %}
|
{% else %}
|
||||||
{% endif %}
|
{% set index = indexes | first %}
|
||||||
{% if not indexes %}
|
{% set class = "md-nav__link--active" if index == page %}
|
||||||
<label class="md-nav__link" for="{{ path }}" id="{{ path }}_label" tabindex="0">
|
<div class="md-nav__link md-nav__container">
|
||||||
{{ nav_item.title }}
|
<a href="{{ index.url | url }}" class="md-nav__link {{ class }}">
|
||||||
<span class="md-nav__icon md-icon"></span>
|
{{ render_content(index, nav_item) }}
|
||||||
</label>
|
</a>
|
||||||
{% else %}
|
{% if nav_item.children | length > 1 %}
|
||||||
{% set index = indexes | first %}
|
<label class="md-nav__link {{ class }}" for="{{ path }}">
|
||||||
{% set class = "md-nav__link--active" if index == page %}
|
<span class="md-nav__icon md-icon"></span>
|
||||||
<div class="md-nav__link md-nav__link--index {{ class }}">
|
</label>
|
||||||
<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) }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</ul>
|
{% endif %}
|
||||||
</nav>
|
<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>
|
</li>
|
||||||
{% elif nav_item == page %}
|
{% elif nav_item == page %}
|
||||||
<li class="{{ class }}">
|
<li class="{{ class }}">
|
||||||
@ -69,12 +111,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if toc %}
|
{% if toc %}
|
||||||
<label class="md-nav__link md-nav__link--active" for="__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>
|
<span class="md-nav__icon md-icon"></span>
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ nav_item.url | url }}" class="md-nav__link md-nav__link--active">
|
<a href="{{ nav_item.url | url }}" class="md-nav__link md-nav__link--active">
|
||||||
{{ nav_item.title }}
|
{{ render_content(nav_item) }}
|
||||||
</a>
|
</a>
|
||||||
{% if toc %}
|
{% if toc %}
|
||||||
{% include "partials/toc.html" %}
|
{% include "partials/toc.html" %}
|
||||||
@ -83,9 +125,8 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<li class="{{ class }}">
|
<li class="{{ class }}">
|
||||||
<a href="{{ nav_item.url | url }}" class="md-nav__link">
|
<a href="{{ nav_item.url | url }}" class="md-nav__link">
|
||||||
{{ nav_item.title }}
|
{{ render_content(nav_item) }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
{{ render(nav_item, path, level) }}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{#-
|
{#-
|
||||||
This file was automatically generated - do not edit
|
This file was automatically generated - do not edit
|
||||||
-#}
|
-#}
|
||||||
|
{% import "partials/nav-item.html" as item with context %}
|
||||||
{% set class = "md-nav md-nav--primary" %}
|
{% set class = "md-nav md-nav--primary" %}
|
||||||
{% if "navigation.tabs" in features %}
|
{% if "navigation.tabs" in features %}
|
||||||
{% set class = class ~ " md-nav--lifted" %}
|
{% set class = class ~ " md-nav--lifted" %}
|
||||||
@ -23,8 +24,7 @@
|
|||||||
<ul class="md-nav__list" data-md-scrollfix>
|
<ul class="md-nav__list" data-md-scrollfix>
|
||||||
{% for nav_item in nav %}
|
{% for nav_item in nav %}
|
||||||
{% set path = "__nav_" ~ loop.index %}
|
{% set path = "__nav_" ~ loop.index %}
|
||||||
{% set level = 1 %}
|
{{ item.render(nav_item, path, 1) }}
|
||||||
{% include "partials/nav-item.html" %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</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>
|
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.meta.date }}">
|
||||||
|
{{- post.meta.date_format -}}
|
||||||
|
</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.meta.readtime %}
|
||||||
|
{% set time = post.meta.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.meta.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>
|
@ -1,28 +1,35 @@
|
|||||||
{#-
|
{#-
|
||||||
This file was automatically generated - do not edit
|
This file was automatically generated - do not edit
|
||||||
-#}
|
-#}
|
||||||
{% if not class %}
|
{% 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__link" %}
|
{% set class = "md-tabs__link" %}
|
||||||
{% if nav_item.active %}
|
{% if ref.active %}
|
||||||
{% set class = class ~ " md-tabs__link--active" %}
|
{% set class = class ~ " md-tabs__link--active" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if nav_item.children %}
|
|
||||||
{% set title = title | d(nav_item.title) %}
|
|
||||||
{% set nav_item = nav_item.children | first %}
|
|
||||||
{% if nav_item.children %}
|
{% if nav_item.children %}
|
||||||
{% include "partials/tabs-item.html" %}
|
{% set first = nav_item.children | first %}
|
||||||
|
{% if first.children %}
|
||||||
|
{{ render(first, ref) }}
|
||||||
|
{% else %}
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="{{ first.url | url }}" class="{{ class }}">
|
||||||
|
{{ render_content(first, ref) }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="md-tabs__item">
|
<li class="md-tabs__item">
|
||||||
<a href="{{ nav_item.url | url }}" class="{{ class }}">
|
<a href="{{ nav_item.url | url }}" class="{{ class }}">
|
||||||
{{ title }}
|
{{ render_content(nav_item) }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% endmacro %}
|
||||||
<li class="md-tabs__item">
|
|
||||||
<a href="{{ nav_item.url | url }}" class="{{ class }}">
|
|
||||||
{{ nav_item.title }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
{#-
|
{#-
|
||||||
This file was automatically generated - do not edit
|
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">
|
<nav class="md-tabs" aria-label="{{ lang.t('tabs') }}" data-md-component="tabs">
|
||||||
<div class="md-grid">
|
<div class="md-grid">
|
||||||
<ul class="md-tabs__list">
|
<ul class="md-tabs__list">
|
||||||
{% for nav_item in nav %}
|
{% for nav_item in nav %}
|
||||||
{% include "partials/tabs-item.html" %}
|
{{ item.render(nav_item) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
0
material/plugins/blog/__init__.py
Normal file
0
material/plugins/blog/__init__.py
Normal file
82
material/plugins/blog/config.py
Normal file
82
material/plugins/blog/config.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Blog plugin configuration scheme
|
||||||
|
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_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_template = Type(str, default = "~2~")
|
||||||
|
|
||||||
|
# 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)
|
887
material/plugins/blog/plugin.py
Normal file
887
material/plugins/blog/plugin.py
Normal file
@ -0,0 +1,887 @@
|
|||||||
|
# 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 paginate
|
||||||
|
import posixpath
|
||||||
|
import re
|
||||||
|
import readtime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from babel.dates import format_date
|
||||||
|
from copy import copy
|
||||||
|
from datetime import date, datetime, time
|
||||||
|
from hashlib import sha1
|
||||||
|
from lxml.html import fragment_fromstring, tostring
|
||||||
|
from mkdocs import utils
|
||||||
|
from mkdocs.utils.meta import get_data
|
||||||
|
from mkdocs.commands.build import _populate_page
|
||||||
|
from mkdocs.contrib.search import SearchIndex
|
||||||
|
from mkdocs.plugins import BasePlugin
|
||||||
|
from mkdocs.structure.files import File, Files
|
||||||
|
from mkdocs.structure.nav import Link, Section
|
||||||
|
from mkdocs.structure.pages import Page
|
||||||
|
from tempfile import gettempdir
|
||||||
|
from yaml import SafeLoader, load
|
||||||
|
|
||||||
|
from material.plugins.blog.config import BlogConfig
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Blog plugin
|
||||||
|
class BlogPlugin(BasePlugin[BlogConfig]):
|
||||||
|
supports_multiple_instances = True
|
||||||
|
|
||||||
|
# Determine whether we're running under dirty reload
|
||||||
|
def on_startup(self, *, command, dirty):
|
||||||
|
self.is_serve = (command == "serve")
|
||||||
|
self.is_dirtyreload = False
|
||||||
|
self.is_dirty = dirty
|
||||||
|
|
||||||
|
# Initialize plugin
|
||||||
|
def on_config(self, config):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Resolve source directory for posts and generated files
|
||||||
|
self.post_dir = self._resolve("posts")
|
||||||
|
self.temp_dir = gettempdir()
|
||||||
|
|
||||||
|
# Initialize posts
|
||||||
|
self.post_map = dict()
|
||||||
|
self.post_meta_map = dict()
|
||||||
|
self.post_pages = []
|
||||||
|
self.post_pager_pages = []
|
||||||
|
|
||||||
|
# Initialize archive
|
||||||
|
if self.config.archive:
|
||||||
|
self.archive_map = dict()
|
||||||
|
self.archive_post_map = dict()
|
||||||
|
|
||||||
|
# Initialize categories
|
||||||
|
if self.config.categories:
|
||||||
|
self.category_map = dict()
|
||||||
|
self.category_name_map = dict()
|
||||||
|
self.category_post_map = dict()
|
||||||
|
|
||||||
|
# Initialize authors
|
||||||
|
if self.config.authors:
|
||||||
|
self.authors_map = dict()
|
||||||
|
|
||||||
|
# Resolve authors file
|
||||||
|
path = os.path.normpath(os.path.join(
|
||||||
|
config.docs_dir,
|
||||||
|
self.config.authors_file.format(
|
||||||
|
blog = self.config.blog_dir
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Load authors map, if it exists
|
||||||
|
if os.path.isfile(path):
|
||||||
|
with open(path, encoding = "utf-8") as f:
|
||||||
|
self.authors_map = load(f, SafeLoader) or {}
|
||||||
|
|
||||||
|
# Ensure that format strings have no trailing slashes
|
||||||
|
for option in [
|
||||||
|
"post_url_format",
|
||||||
|
"archive_url_format",
|
||||||
|
"categories_url_format",
|
||||||
|
"pagination_url_format"
|
||||||
|
]:
|
||||||
|
if self.config[option].endswith("/"):
|
||||||
|
log.error(f"Option '{option}' must not contain trailing slash.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Inherit global table of contents setting
|
||||||
|
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
|
||||||
|
|
||||||
|
# If pagination should not be used, set to large value
|
||||||
|
if not self.config.pagination:
|
||||||
|
self.config.pagination_per_page = 1e7
|
||||||
|
|
||||||
|
# By default, drafts are rendered when the documentation is served,
|
||||||
|
# but not when it is built. This should nicely align with the expected
|
||||||
|
# user experience when authoring documentation.
|
||||||
|
if self.is_serve and self.config.draft_on_serve:
|
||||||
|
self.config.draft = True
|
||||||
|
|
||||||
|
# Adjust paths to assets in the posts directory and preprocess posts
|
||||||
|
def on_files(self, files, *, config):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Adjust destination paths for assets
|
||||||
|
path = self._resolve("assets")
|
||||||
|
for file in files.media_files():
|
||||||
|
if self.post_dir not in file.src_uri:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Compute destination URL
|
||||||
|
file.url = file.url.replace(self.post_dir, path)
|
||||||
|
|
||||||
|
# Compute destination file system path
|
||||||
|
file.dest_uri = file.dest_uri.replace(self.post_dir, path)
|
||||||
|
file.abs_dest_path = os.path.join(config.site_dir, file.dest_path)
|
||||||
|
|
||||||
|
# Hack: as post URLs are dynamically computed and can be configured by
|
||||||
|
# the author, we need to compute them before we process the contents of
|
||||||
|
# any other page or post. If we wouldn't do that, URLs would be invalid
|
||||||
|
# and we would need to patch them afterwards. The only way to do this
|
||||||
|
# correctly is to first extract the metadata of all posts. Additionally,
|
||||||
|
# while we're at it, generate all archive and category pages as we have
|
||||||
|
# the post metadata on our hands. This ensures that we can safely link
|
||||||
|
# from anywhere to all pages that are generated as part of the blog.
|
||||||
|
for file in files.documentation_pages():
|
||||||
|
if self.post_dir not in file.src_uri:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Read and preprocess post
|
||||||
|
with open(file.abs_src_path, encoding = "utf-8") as f:
|
||||||
|
markdown, meta = get_data(f.read())
|
||||||
|
|
||||||
|
# Ensure post has a date set
|
||||||
|
if not meta.get("date"):
|
||||||
|
log.error(f"Blog post '{file.src_uri}' has no date set.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Compute slug from metadata, content or file name
|
||||||
|
headline = utils.get_markdown_title(markdown)
|
||||||
|
slug = meta.get("title", headline or file.name)
|
||||||
|
|
||||||
|
# Front matter can be defind in YAML, guarded by two lines with
|
||||||
|
# `---` markers, or MultiMarkdown, separated by an empty line.
|
||||||
|
# If the author chooses to use MultiMarkdown syntax, date is
|
||||||
|
# returned as a string, which is different from YAML behavior,
|
||||||
|
# which returns a date. Thus, we must check for its type, and
|
||||||
|
# parse the date for normalization purposes.
|
||||||
|
if isinstance(meta["date"], str):
|
||||||
|
meta["date"] = date.fromisoformat(meta["date"])
|
||||||
|
|
||||||
|
# Normalize date to datetime for proper sorting
|
||||||
|
if not isinstance(meta["date"], datetime):
|
||||||
|
meta["date"] = datetime.combine(meta["date"], time())
|
||||||
|
|
||||||
|
# Compute category slugs
|
||||||
|
categories = []
|
||||||
|
for name in meta.get("categories", []):
|
||||||
|
categories.append(self.config.categories_slugify(
|
||||||
|
name, self.config.categories_slugify_separator
|
||||||
|
))
|
||||||
|
|
||||||
|
# Check if maximum number of categories is reached
|
||||||
|
max_categories = self.config.post_url_max_categories
|
||||||
|
if len(categories) == max_categories:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Compute path from format string
|
||||||
|
date_format = self.config.post_url_date_format
|
||||||
|
path = self.config.post_url_format.format(
|
||||||
|
categories = "/".join(categories),
|
||||||
|
date = self._format_date(meta["date"], date_format, config),
|
||||||
|
file = file.name,
|
||||||
|
slug = meta.get("slug", self.config.post_slugify(
|
||||||
|
slug, self.config.post_slugify_separator
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize path, as it may begin with a slash
|
||||||
|
path = posixpath.normpath("/".join([".", path]))
|
||||||
|
|
||||||
|
# Compute destination URL according to settings
|
||||||
|
file.url = self._resolve(path)
|
||||||
|
if not config.use_directory_urls:
|
||||||
|
file.url += ".html"
|
||||||
|
else:
|
||||||
|
file.url += "/"
|
||||||
|
|
||||||
|
# Compute destination file system path
|
||||||
|
file.dest_uri = re.sub(r"(?<=\/)$", "index.html", file.url)
|
||||||
|
file.abs_dest_path = os.path.join(
|
||||||
|
config.site_dir, file.dest_path
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add post metadata
|
||||||
|
self.post_meta_map[file.src_uri] = meta
|
||||||
|
|
||||||
|
# Sort post metadata by date (descending)
|
||||||
|
self.post_meta_map = dict(sorted(
|
||||||
|
self.post_meta_map.items(),
|
||||||
|
key = lambda item: item[1]["date"], reverse = True
|
||||||
|
))
|
||||||
|
|
||||||
|
# Find and extract the section hosting the blog
|
||||||
|
path = self._resolve("index.md")
|
||||||
|
root = _host(config.nav, path)
|
||||||
|
|
||||||
|
# Ensure blog root exists
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if not file:
|
||||||
|
log.error(f"Blog root '{path}' does not exist.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Ensure blog root is part of navigation
|
||||||
|
if not root:
|
||||||
|
log.error(f"Blog root '{path}' not in navigation.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Generate and register files for archive
|
||||||
|
if self.config.archive:
|
||||||
|
name = self._translate(config, self.config.archive_name)
|
||||||
|
data = self._generate_files_for_archive(config, files)
|
||||||
|
if data:
|
||||||
|
root.append({ name: data })
|
||||||
|
|
||||||
|
# Generate and register files for categories
|
||||||
|
if self.config.categories:
|
||||||
|
name = self._translate(config, self.config.categories_name)
|
||||||
|
data = self._generate_files_for_categories(config, files)
|
||||||
|
if data:
|
||||||
|
root.append({ name: data })
|
||||||
|
|
||||||
|
# Hack: add posts temporarily, so MkDocs doesn't complain
|
||||||
|
name = sha1(path.encode("utf-8")).hexdigest()
|
||||||
|
root.append({
|
||||||
|
f"__posts_${name}": list(self.post_meta_map.keys())
|
||||||
|
})
|
||||||
|
|
||||||
|
# Cleanup navigation before proceeding
|
||||||
|
def on_nav(self, nav, *, config, files):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find and resolve index for cleanup
|
||||||
|
path = self._resolve("index.md")
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
|
||||||
|
# Determine blog root section
|
||||||
|
self.main = file.page
|
||||||
|
if self.main.parent:
|
||||||
|
root = self.main.parent.children
|
||||||
|
else:
|
||||||
|
root = nav.items
|
||||||
|
|
||||||
|
# Hack: remove temporarily added posts from the navigation
|
||||||
|
name = sha1(path.encode("utf-8")).hexdigest()
|
||||||
|
for item in root:
|
||||||
|
if not item.is_section or item.title != f"__posts_${name}":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Detach previous and next links of posts
|
||||||
|
if item.children:
|
||||||
|
head = item.children[+0]
|
||||||
|
tail = item.children[-1]
|
||||||
|
|
||||||
|
# Link page prior to posts to page after posts
|
||||||
|
if head.previous_page:
|
||||||
|
head.previous_page.next_page = tail.next_page
|
||||||
|
|
||||||
|
# Link page after posts to page prior to posts
|
||||||
|
if tail.next_page:
|
||||||
|
tail.next_page.previous_page = head.previous_page
|
||||||
|
|
||||||
|
# Contain previous and next links inside posts
|
||||||
|
head.previous_page = None
|
||||||
|
tail.next_page = None
|
||||||
|
|
||||||
|
# Set blog as parent page
|
||||||
|
for page in item.children:
|
||||||
|
page.parent = self.main
|
||||||
|
next = page.next_page
|
||||||
|
|
||||||
|
# Switch previous and next links
|
||||||
|
page.next_page = page.previous_page
|
||||||
|
page.previous_page = next
|
||||||
|
|
||||||
|
# Remove posts from navigation
|
||||||
|
root.remove(item)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Prepare post for rendering
|
||||||
|
def on_page_markdown(self, markdown, *, page, config, files):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only process posts
|
||||||
|
if self.post_dir not in page.file.src_uri:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip processing of drafts
|
||||||
|
if self._is_draft(page.file.src_uri):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure template is set or use default
|
||||||
|
if "template" not in page.meta:
|
||||||
|
page.meta["template"] = "blog-post.html"
|
||||||
|
|
||||||
|
# Use previously normalized date
|
||||||
|
page.meta["date"] = self.post_meta_map[page.file.src_uri]["date"]
|
||||||
|
|
||||||
|
# Ensure navigation is hidden
|
||||||
|
page.meta["hide"] = page.meta.get("hide", [])
|
||||||
|
if "navigation" not in page.meta["hide"]:
|
||||||
|
page.meta["hide"].append("navigation")
|
||||||
|
|
||||||
|
# Format date for rendering
|
||||||
|
date_format = self.config.post_date_format
|
||||||
|
page.meta["date_format"] = self._format_date(
|
||||||
|
page.meta["date"], date_format, config
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compute readtime if desired and not explicitly set
|
||||||
|
if self.config.post_readtime:
|
||||||
|
|
||||||
|
# There's a bug in the readtime library, which causes it to fail
|
||||||
|
# when the input string contains emojis (reported in #5555)
|
||||||
|
encoded = markdown.encode("unicode_escape")
|
||||||
|
if "readtime" not in page.meta:
|
||||||
|
rate = self.config.post_readtime_words_per_minute
|
||||||
|
read = readtime.of_markdown(encoded, rate)
|
||||||
|
page.meta["readtime"] = read.minutes
|
||||||
|
|
||||||
|
# Compute post categories
|
||||||
|
page.categories = []
|
||||||
|
if self.config.categories:
|
||||||
|
for name in page.meta.get("categories", []):
|
||||||
|
file = files.get_file_from_path(self.category_name_map[name])
|
||||||
|
page.categories.append(file.page)
|
||||||
|
|
||||||
|
# Compute post authors
|
||||||
|
page.authors = []
|
||||||
|
if self.config.authors:
|
||||||
|
for name in page.meta.get("authors", []):
|
||||||
|
if name not in self.authors_map:
|
||||||
|
log.error(
|
||||||
|
f"Blog post '{page.file.src_uri}' author '{name}' "
|
||||||
|
f"unknown, not listed in .authors.yml"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Add author to page
|
||||||
|
page.authors.append(self.authors_map[name])
|
||||||
|
|
||||||
|
# Fix stale link if previous post is a draft
|
||||||
|
prev = page.previous_page
|
||||||
|
while prev and self._is_draft(prev.file.src_uri):
|
||||||
|
page.previous_page = prev.previous_page
|
||||||
|
prev = prev.previous_page
|
||||||
|
|
||||||
|
# Fix stale link if next post is a draft
|
||||||
|
next = page.next_page
|
||||||
|
while next and self._is_draft(next.file.src_uri):
|
||||||
|
page.next_page = next.next_page
|
||||||
|
next = next.next_page
|
||||||
|
|
||||||
|
# Filter posts and generate excerpts for generated pages
|
||||||
|
def on_env(self, env, *, config, files):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip post excerpts on dirty reload to save time
|
||||||
|
if self.is_dirtyreload:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Copy configuration and enable 'toc' extension
|
||||||
|
config = copy(config)
|
||||||
|
config.mdx_configs["toc"] = copy(config.mdx_configs.get("toc", {}))
|
||||||
|
|
||||||
|
# Ensure that post titles are links
|
||||||
|
config.mdx_configs["toc"]["anchorlink"] = True
|
||||||
|
config.mdx_configs["toc"]["permalink"] = False
|
||||||
|
|
||||||
|
# Filter posts that should not be published
|
||||||
|
for file in files.documentation_pages():
|
||||||
|
if self.post_dir in file.src_uri:
|
||||||
|
if self._is_draft(file.src_uri):
|
||||||
|
files.remove(file)
|
||||||
|
|
||||||
|
# Ensure template is set
|
||||||
|
if "template" not in self.main.meta:
|
||||||
|
self.main.meta["template"] = "blog.html"
|
||||||
|
|
||||||
|
# Populate archive
|
||||||
|
if self.config.archive:
|
||||||
|
for path in self.archive_map:
|
||||||
|
self.archive_post_map[path] = []
|
||||||
|
|
||||||
|
# Generate post excerpts for archive
|
||||||
|
base = files.get_file_from_path(path)
|
||||||
|
for file in self.archive_map[path]:
|
||||||
|
self.archive_post_map[path].append(
|
||||||
|
self._generate_excerpt(file, base, config, files)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure template is set
|
||||||
|
page = base.page
|
||||||
|
if "template" not in page.meta:
|
||||||
|
page.meta["template"] = "blog-archive.html"
|
||||||
|
|
||||||
|
# Populate categories
|
||||||
|
if self.config.categories:
|
||||||
|
for path in self.category_map:
|
||||||
|
self.category_post_map[path] = []
|
||||||
|
|
||||||
|
# Generate post excerpts for categories
|
||||||
|
base = files.get_file_from_path(path)
|
||||||
|
for file in self.category_map[path]:
|
||||||
|
self.category_post_map[path].append(
|
||||||
|
self._generate_excerpt(file, base, config, files)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure template is set
|
||||||
|
page = base.page
|
||||||
|
if "template" not in page.meta:
|
||||||
|
page.meta["template"] = "blog-category.html"
|
||||||
|
|
||||||
|
# Resolve path of initial index
|
||||||
|
curr = self._resolve("index.md")
|
||||||
|
base = self.main.file
|
||||||
|
|
||||||
|
# Initialize index
|
||||||
|
self.post_map[curr] = []
|
||||||
|
self.post_pager_pages.append(self.main)
|
||||||
|
|
||||||
|
# Generate indexes by paginating through posts
|
||||||
|
for path in self.post_meta_map.keys():
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if not self._is_draft(path):
|
||||||
|
self.post_pages.append(file.page)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Generate new index when the current is full
|
||||||
|
per_page = self.config.pagination_per_page
|
||||||
|
if len(self.post_map[curr]) == per_page:
|
||||||
|
offset = 1 + len(self.post_map)
|
||||||
|
|
||||||
|
# Resolve path of new index
|
||||||
|
curr = self.config.pagination_url_format.format(page = offset)
|
||||||
|
curr = self._resolve(curr + ".md")
|
||||||
|
|
||||||
|
# Generate file
|
||||||
|
self._generate_file(curr, f"# {self.main.title}")
|
||||||
|
|
||||||
|
# Register file and page
|
||||||
|
base = self._register_file(curr, config, files)
|
||||||
|
page = self._register_page(base, config, files)
|
||||||
|
|
||||||
|
# Inherit page metadata, title and position
|
||||||
|
page.meta = self.main.meta
|
||||||
|
page.title = self.main.title
|
||||||
|
page.parent = self.main
|
||||||
|
page.previous_page = self.main.previous_page
|
||||||
|
page.next_page = self.main.next_page
|
||||||
|
|
||||||
|
# Initialize next index
|
||||||
|
self.post_map[curr] = []
|
||||||
|
self.post_pager_pages.append(page)
|
||||||
|
|
||||||
|
# Assign post excerpt to current index
|
||||||
|
self.post_map[curr].append(
|
||||||
|
self._generate_excerpt(file, base, config, files)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Populate generated pages
|
||||||
|
def on_page_context(self, context, *, page, config, nav):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Provide post excerpts for index
|
||||||
|
path = page.file.src_uri
|
||||||
|
if path in self.post_map:
|
||||||
|
context["posts"] = self.post_map[path]
|
||||||
|
if self.config.blog_toc:
|
||||||
|
self._populate_toc(page, context["posts"])
|
||||||
|
|
||||||
|
# Create pagination
|
||||||
|
pagination = paginate.Page(
|
||||||
|
self.post_pages,
|
||||||
|
page = list(self.post_map.keys()).index(path) + 1,
|
||||||
|
items_per_page = self.config.pagination_per_page,
|
||||||
|
url_maker = lambda n: utils.get_relative_url(
|
||||||
|
self.post_pager_pages[n - 1].url,
|
||||||
|
page.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create pagination pager
|
||||||
|
context["pagination"] = lambda args: pagination.pager(
|
||||||
|
format = self.config.pagination_template,
|
||||||
|
show_if_single_page = False,
|
||||||
|
**args
|
||||||
|
)
|
||||||
|
|
||||||
|
# Provide post excerpts for archive
|
||||||
|
if self.config.archive:
|
||||||
|
if path in self.archive_post_map:
|
||||||
|
context["posts"] = self.archive_post_map[path]
|
||||||
|
if self.config.archive_toc:
|
||||||
|
self._populate_toc(page, context["posts"])
|
||||||
|
|
||||||
|
# Provide post excerpts for categories
|
||||||
|
if self.config.categories:
|
||||||
|
if path in self.category_post_map:
|
||||||
|
context["posts"] = self.category_post_map[path]
|
||||||
|
if self.config.categories_toc:
|
||||||
|
self._populate_toc(page, context["posts"])
|
||||||
|
|
||||||
|
# Determine whether we're running under dirty reload
|
||||||
|
def on_serve(self, server, *, config, builder):
|
||||||
|
self.is_dirtyreload = self.is_dirty
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Generate and register files for archive
|
||||||
|
def _generate_files_for_archive(self, config, files):
|
||||||
|
for path, meta in self.post_meta_map.items():
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if self._is_draft(path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Compute name from format string
|
||||||
|
date_format = self.config.archive_date_format
|
||||||
|
name = self._format_date(meta["date"], date_format, config)
|
||||||
|
|
||||||
|
# Compute path from format string
|
||||||
|
date_format = self.config.archive_url_date_format
|
||||||
|
path = self.config.archive_url_format.format(
|
||||||
|
date = self._format_date(meta["date"], date_format, config)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create file for archive if it doesn't exist
|
||||||
|
path = self._resolve(path + ".md")
|
||||||
|
if path not in self.archive_map:
|
||||||
|
self.archive_map[path] = []
|
||||||
|
|
||||||
|
# Generate and register file for archive
|
||||||
|
self._generate_file(path, f"# {name}")
|
||||||
|
self._register_file(path, config, files)
|
||||||
|
|
||||||
|
# Assign current post to archive
|
||||||
|
self.archive_map[path].append(file)
|
||||||
|
|
||||||
|
# Return generated archive files
|
||||||
|
return list(self.archive_map.keys())
|
||||||
|
|
||||||
|
# Generate and register files for categories
|
||||||
|
def _generate_files_for_categories(self, config, files):
|
||||||
|
allowed = set(self.config.categories_allowed)
|
||||||
|
for path, meta in self.post_meta_map.items():
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if self._is_draft(path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ensure category is in (non-empty) allow list
|
||||||
|
categories = set(meta.get("categories", []))
|
||||||
|
if allowed:
|
||||||
|
for name in categories - allowed:
|
||||||
|
log.error(
|
||||||
|
f"Blog post '{file.src_uri}' uses a category "
|
||||||
|
f"which is not in allow list: {name}"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Traverse all categories of the post
|
||||||
|
for name in categories:
|
||||||
|
path = self.config.categories_url_format.format(
|
||||||
|
slug = self.config.categories_slugify(
|
||||||
|
name, self.config.categories_slugify_separator
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create file for category if it doesn't exist
|
||||||
|
path = self._resolve(path + ".md")
|
||||||
|
if path not in self.category_map:
|
||||||
|
self.category_map[path] = []
|
||||||
|
|
||||||
|
# Generate and register file for category
|
||||||
|
self._generate_file(path, f"# {name}")
|
||||||
|
self._register_file(path, config, files)
|
||||||
|
|
||||||
|
# Link category path to name
|
||||||
|
self.category_name_map[name] = path
|
||||||
|
|
||||||
|
# Assign current post to category
|
||||||
|
self.category_map[path].append(file)
|
||||||
|
|
||||||
|
# Sort categories alphabetically (ascending)
|
||||||
|
self.category_map = dict(sorted(self.category_map.items()))
|
||||||
|
|
||||||
|
# Return generated category files
|
||||||
|
return list(self.category_map.keys())
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Check if a post is a draft
|
||||||
|
def _is_draft(self, path):
|
||||||
|
meta = self.post_meta_map[path]
|
||||||
|
if not self.config.draft:
|
||||||
|
|
||||||
|
# Check if post date is in the future
|
||||||
|
future = False
|
||||||
|
if self.config.draft_if_future_date:
|
||||||
|
future = meta["date"] > datetime.now()
|
||||||
|
|
||||||
|
# Check if post is marked as draft
|
||||||
|
return meta.get("draft", future)
|
||||||
|
|
||||||
|
# Post is not a draft
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Generate a post excerpt relative to base
|
||||||
|
def _generate_excerpt(self, file, base, config, files):
|
||||||
|
page = file.page
|
||||||
|
|
||||||
|
# Generate temporary file and page for post excerpt
|
||||||
|
temp = self._register_file(file.src_uri, config)
|
||||||
|
excerpt = Page(page.title, temp, config)
|
||||||
|
|
||||||
|
# Check for separator, if post excerpt is required
|
||||||
|
separator = self.config.post_excerpt_separator
|
||||||
|
if self.config.post_excerpt == "required":
|
||||||
|
if separator not in page.markdown:
|
||||||
|
log.error(f"Blog post '{temp.src_uri}' has no excerpt.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Ensure separator at the end to strip footnotes and patch h1-h5
|
||||||
|
markdown = "\n\n".join([page.markdown, separator])
|
||||||
|
markdown = re.sub(r"(^#{1,5})", "#\\1", markdown, flags = re.MULTILINE)
|
||||||
|
|
||||||
|
# Extract content and metadata from original post
|
||||||
|
excerpt.file.url = base.url
|
||||||
|
excerpt.markdown = markdown
|
||||||
|
excerpt.meta = page.meta
|
||||||
|
|
||||||
|
# Render post and revert page URL
|
||||||
|
excerpt.render(config, files)
|
||||||
|
excerpt.file.url = page.url
|
||||||
|
|
||||||
|
# Find all anchor links
|
||||||
|
expr = re.compile(
|
||||||
|
r"<a[^>]+href=['\"]?#[^>]+>",
|
||||||
|
re.IGNORECASE | re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace callback
|
||||||
|
first = True
|
||||||
|
def replace(match):
|
||||||
|
value = match.group()
|
||||||
|
|
||||||
|
# Handle anchor link
|
||||||
|
el = fragment_fromstring(value.encode("utf-8"))
|
||||||
|
if el.tag == "a":
|
||||||
|
nonlocal first
|
||||||
|
|
||||||
|
# Fix up each anchor link of the excerpt with a link to the
|
||||||
|
# anchor of the actual post, except for the first one – that
|
||||||
|
# one needs to go to the top of the post. A better way might
|
||||||
|
# be a Markdown extension, but for now this should be fine.
|
||||||
|
url = utils.get_relative_url(excerpt.file.url, base.url)
|
||||||
|
if first:
|
||||||
|
el.set("href", url)
|
||||||
|
else:
|
||||||
|
el.set("href", url + el.get("href"))
|
||||||
|
|
||||||
|
# From now on reference anchors
|
||||||
|
first = False
|
||||||
|
|
||||||
|
# Replace link opening tag (without closing tag)
|
||||||
|
return tostring(el, encoding = "unicode")[:-4]
|
||||||
|
|
||||||
|
# Extract excerpt from post and replace anchor links
|
||||||
|
excerpt.content = expr.sub(
|
||||||
|
replace,
|
||||||
|
excerpt.content.split(separator)[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine maximum number of authors and categories
|
||||||
|
max_authors = self.config.post_excerpt_max_authors
|
||||||
|
max_categories = self.config.post_excerpt_max_categories
|
||||||
|
|
||||||
|
# Obtain computed metadata from original post
|
||||||
|
excerpt.authors = page.authors[:max_authors]
|
||||||
|
excerpt.categories = page.categories[:max_categories]
|
||||||
|
|
||||||
|
# Return post excerpt
|
||||||
|
return excerpt
|
||||||
|
|
||||||
|
# Generate a file with the given template and content
|
||||||
|
def _generate_file(self, path, content):
|
||||||
|
content = f"---\nsearch:\n exclude: true\n---\n\n{content}"
|
||||||
|
utils.write_file(
|
||||||
|
bytes(content, "utf-8"),
|
||||||
|
os.path.join(self.temp_dir, path)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register a file
|
||||||
|
def _register_file(self, path, config, files = Files([])):
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if not file:
|
||||||
|
urls = config.use_directory_urls
|
||||||
|
file = File(path, self.temp_dir, config.site_dir, urls)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# for plugins that generate files which was introduced by the
|
||||||
|
# git-revision-date-localized-plugin - see https://bit.ly/3ZUmdBx
|
||||||
|
file.generated_by = "material/blog"
|
||||||
|
|
||||||
|
# Return file
|
||||||
|
return file
|
||||||
|
|
||||||
|
# Register and populate a page
|
||||||
|
def _register_page(self, file, config, files):
|
||||||
|
page = Page(None, file, config)
|
||||||
|
_populate_page(page, config, files)
|
||||||
|
return page
|
||||||
|
|
||||||
|
# Populate table of contents of given page
|
||||||
|
def _populate_toc(self, page, posts):
|
||||||
|
toc = page.toc.items[0]
|
||||||
|
for post in posts:
|
||||||
|
toc.children.append(post.toc.items[0])
|
||||||
|
|
||||||
|
# Remove anchors below the second level
|
||||||
|
post.toc.items[0].children = []
|
||||||
|
|
||||||
|
# Translate the given placeholder value
|
||||||
|
def _translate(self, config, value):
|
||||||
|
env = config.theme.get_env()
|
||||||
|
|
||||||
|
# Load language template and return translation for placeholder
|
||||||
|
language = "partials/language.html"
|
||||||
|
template = env.get_template(language, None, { "config": config })
|
||||||
|
return template.module.t(value)
|
||||||
|
|
||||||
|
# Resolve path relative to blog root
|
||||||
|
def _resolve(self, *args):
|
||||||
|
path = posixpath.join(self.config.blog_dir, *args)
|
||||||
|
return posixpath.normpath(path)
|
||||||
|
|
||||||
|
# Format date according to locale
|
||||||
|
def _format_date(self, date, format, config):
|
||||||
|
return format_date(
|
||||||
|
date,
|
||||||
|
format = format,
|
||||||
|
locale = config.theme["language"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Helper functions
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Search the given navigation section (from the configuration) recursively to
|
||||||
|
# find the section to host all generated pages (archive, categories, etc.)
|
||||||
|
def _host(nav, path):
|
||||||
|
|
||||||
|
# Search navigation dictionary
|
||||||
|
if isinstance(nav, dict):
|
||||||
|
for _, item in nav.items():
|
||||||
|
result = _host(item, path)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Search navigation list
|
||||||
|
elif isinstance(nav, list):
|
||||||
|
if path in nav:
|
||||||
|
return nav
|
||||||
|
|
||||||
|
# Search each list item
|
||||||
|
for item in nav:
|
||||||
|
if isinstance(item, dict) and path in item.values():
|
||||||
|
if path in item.values():
|
||||||
|
return nav
|
||||||
|
else:
|
||||||
|
result = _host(item, path)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Copied and adapted from MkDocs, because we need to return existing pages and
|
||||||
|
# support anchor names as subtitles, which is pretty fucking cool.
|
||||||
|
def _data_to_navigation(nav, config, files):
|
||||||
|
|
||||||
|
# Search navigation dictionary
|
||||||
|
if isinstance(nav, dict):
|
||||||
|
return [
|
||||||
|
_data_to_navigation((key, value), config, files)
|
||||||
|
if isinstance(value, str) else
|
||||||
|
Section(
|
||||||
|
title = key,
|
||||||
|
children = _data_to_navigation(value, config, files)
|
||||||
|
)
|
||||||
|
for key, value in nav.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Search navigation list
|
||||||
|
elif isinstance(nav, list):
|
||||||
|
return [
|
||||||
|
_data_to_navigation(item, config, files)[0]
|
||||||
|
if isinstance(item, dict) and len(item) == 1 else
|
||||||
|
_data_to_navigation(item, config, files)
|
||||||
|
for item in nav
|
||||||
|
]
|
||||||
|
|
||||||
|
# Extract navigation title and path and split anchors
|
||||||
|
title, path = nav if isinstance(nav, tuple) else (None, nav)
|
||||||
|
path, _, anchor = path.partition("#")
|
||||||
|
|
||||||
|
# Try to retrieve existing file
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if not file:
|
||||||
|
return Link(title, path)
|
||||||
|
|
||||||
|
# Use resolved assets destination path
|
||||||
|
if not path.endswith(".md"):
|
||||||
|
return Link(title or os.path.basename(path), file.url)
|
||||||
|
|
||||||
|
# Generate temporary file as for post excerpts
|
||||||
|
else:
|
||||||
|
urls = config.use_directory_urls
|
||||||
|
link = File(path, config.docs_dir, config.site_dir, urls)
|
||||||
|
page = Page(title or file.page.title, link, config)
|
||||||
|
|
||||||
|
# Set destination file system path and URL from original file
|
||||||
|
link.dest_uri = file.dest_uri
|
||||||
|
link.abs_dest_path = file.abs_dest_path
|
||||||
|
link.url = file.url
|
||||||
|
|
||||||
|
# Retrieve name of anchor by misusing the search index
|
||||||
|
if anchor:
|
||||||
|
item = SearchIndex()._find_toc_by_id(file.page.toc, anchor)
|
||||||
|
|
||||||
|
# Set anchor name as subtitle
|
||||||
|
page.meta["subtitle"] = item.title
|
||||||
|
link.url += f"#{anchor}"
|
||||||
|
|
||||||
|
# Return navigation item
|
||||||
|
return page
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Data
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
log = logging.getLogger("mkdocs.material.blog")
|
36
material/plugins/info/config.py
Normal file
36
material/plugins/info/config.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Info plugin configuration scheme
|
||||||
|
class InfoConfig(Config):
|
||||||
|
enabled = Type(bool, default = True)
|
||||||
|
enabled_on_serve = Type(bool, default = False)
|
||||||
|
|
||||||
|
# Options for archive
|
||||||
|
archive = Type(bool, default = True)
|
||||||
|
archive_name = Type(str, default = "example")
|
||||||
|
archive_stop_on_violation = Type(bool, default = True)
|
@ -28,32 +28,19 @@ import sys
|
|||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from mkdocs import utils
|
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.plugins import BasePlugin, event_priority
|
from mkdocs.plugins import BasePlugin, event_priority
|
||||||
from mkdocs.structure.files import get_files
|
from mkdocs.structure.files import get_files
|
||||||
from pkg_resources import get_distribution, working_set
|
from pkg_resources import get_distribution, working_set
|
||||||
from zipfile import ZipFile, ZIP_DEFLATED
|
from zipfile import ZipFile, ZIP_DEFLATED
|
||||||
|
|
||||||
|
from material.plugins.info.config import InfoConfig
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# 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)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Info plugin
|
# Info plugin
|
||||||
class InfoPlugin(BasePlugin[InfoPluginConfig]):
|
class InfoPlugin(BasePlugin[InfoConfig]):
|
||||||
|
|
||||||
# Determine whether we're serving
|
# Determine whether we're serving
|
||||||
def on_startup(self, *, command, dirty):
|
def on_startup(self, *, command, dirty):
|
||||||
@ -235,5 +222,4 @@ def _size(value, factor = 1):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
log = logging.getLogger("mkdocs")
|
log = logging.getLogger("mkdocs.material.info")
|
||||||
log.addFilter(DuplicateFilter())
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Offline plugin configuration scheme
|
||||||
|
class OfflineConfig(Config):
|
||||||
|
enabled = Type(bool, default = True)
|
@ -21,22 +21,16 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from mkdocs import utils
|
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.plugins import BasePlugin, event_priority
|
||||||
|
|
||||||
|
from material.plugins.offline.config import OfflineConfig
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# Class
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Offline plugin configuration scheme
|
|
||||||
class OfflinePluginConfig(Config):
|
|
||||||
enabled = opt.Type(bool, default = True)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Offline plugin
|
# Offline plugin
|
||||||
class OfflinePlugin(BasePlugin[OfflinePluginConfig]):
|
class OfflinePlugin(BasePlugin[OfflineConfig]):
|
||||||
|
|
||||||
# Initialize plugin
|
# Initialize plugin
|
||||||
def on_config(self, config):
|
def on_config(self, config):
|
||||||
|
51
material/plugins/search/config.py
Normal file
51
material/plugins/search/config.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Search pipeline functions
|
||||||
|
pipeline = ("stemmer", "stopWordFilter", "trimmer")
|
||||||
|
|
||||||
|
# Search plugin configuration scheme
|
||||||
|
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,34 +26,22 @@ import regex as re
|
|||||||
from html import escape
|
from html import escape
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from mkdocs import utils
|
from mkdocs import utils
|
||||||
from mkdocs.commands.build import DuplicateFilter
|
from mkdocs.config.config_options import SubConfig
|
||||||
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 mkdocs.plugins import BasePlugin
|
||||||
|
|
||||||
|
from material.plugins.search.config import SearchConfig, SearchFieldConfig
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jieba
|
||||||
|
except ImportError:
|
||||||
|
jieba = None
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# 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")
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Search plugin
|
# Search plugin
|
||||||
class SearchPlugin(BasePlugin[SearchPluginConfig]):
|
class SearchPlugin(BasePlugin[SearchConfig]):
|
||||||
|
|
||||||
# Determine whether we're running under dirty reload
|
# Determine whether we're running under dirty reload
|
||||||
def on_startup(self, *, command, dirty):
|
def on_startup(self, *, command, dirty):
|
||||||
@ -85,6 +73,30 @@ class SearchPlugin(BasePlugin[SearchPluginConfig]):
|
|||||||
# Initialize search index
|
# Initialize search index
|
||||||
self.search_index = SearchIndex(**self.config)
|
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.exists(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.exists(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
|
# Add page to search index
|
||||||
def on_page_context(self, context, *, page, config, nav):
|
def on_page_context(self, context, *, page, config, nav):
|
||||||
self.search_index.add_entry_from_context(page)
|
self.search_index.add_entry_from_context(page)
|
||||||
@ -167,9 +179,10 @@ class SearchIndex:
|
|||||||
title = "".join(section.title).strip()
|
title = "".join(section.title).strip()
|
||||||
text = "".join(section.text).strip()
|
text = "".join(section.text).strip()
|
||||||
|
|
||||||
# Reset text, if only titles should be indexed
|
# Segment Chinese characters if jieba is available
|
||||||
if self.config["indexing"] == "titles":
|
if jieba:
|
||||||
text = ""
|
title = self._segment_chinese(title)
|
||||||
|
text = self._segment_chinese(text)
|
||||||
|
|
||||||
# Create entry for section
|
# Create entry for section
|
||||||
entry = {
|
entry = {
|
||||||
@ -252,6 +265,25 @@ class SearchIndex:
|
|||||||
# No item found
|
# No item found
|
||||||
return None
|
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
|
# HTML element
|
||||||
@ -341,7 +373,8 @@ class Parser(HTMLParser):
|
|||||||
self.keep = set([
|
self.keep = set([
|
||||||
"p", # Paragraphs
|
"p", # Paragraphs
|
||||||
"code", "pre", # Code blocks
|
"code", "pre", # Code blocks
|
||||||
"li", "ol", "ul" # Lists
|
"li", "ol", "ul", # Lists
|
||||||
|
"sub", "sup" # Sub- and superscripts
|
||||||
])
|
])
|
||||||
|
|
||||||
# Current context and section
|
# Current context and section
|
||||||
@ -362,7 +395,7 @@ class Parser(HTMLParser):
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Handle headings
|
# Handle heading
|
||||||
if tag in ([f"h{x}" for x in range(1, 7)]):
|
if tag in ([f"h{x}" for x in range(1, 7)]):
|
||||||
depth = len(self.context)
|
depth = len(self.context)
|
||||||
if "id" in attrs:
|
if "id" in attrs:
|
||||||
@ -507,23 +540,22 @@ class Parser(HTMLParser):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
log = logging.getLogger("mkdocs")
|
log = logging.getLogger("mkdocs.material.search")
|
||||||
log.addFilter(DuplicateFilter())
|
|
||||||
|
|
||||||
# Tags that are self-closing
|
# Tags that are self-closing
|
||||||
void = set([
|
void = set([
|
||||||
"area", # Image map areas
|
"area", # Image map areas
|
||||||
"base", # Document base
|
"base", # Document base
|
||||||
"br", # Line breaks
|
"br", # Line breaks
|
||||||
"col", # Table columns
|
"col", # Table columns
|
||||||
"embed", # External content
|
"embed", # External content
|
||||||
"hr", # Horizontal rules
|
"hr", # Horizontal rules
|
||||||
"img", # Images
|
"img", # Images
|
||||||
"input", # Input fields
|
"input", # Input fields
|
||||||
"link", # Links
|
"link", # Links
|
||||||
"meta", # Metadata
|
"meta", # Metadata
|
||||||
"param", # External parameters
|
"param", # External parameters
|
||||||
"source", # Image source sets
|
"source", # Image source sets
|
||||||
"track", # Text track
|
"track", # Text track
|
||||||
"wbr" # Line break opportunities
|
"wbr" # Line break opportunities
|
||||||
])
|
])
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Social plugin configuration scheme
|
||||||
|
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'"
|
||||||
|
)
|
@ -25,56 +25,26 @@ import os
|
|||||||
import posixpath
|
import posixpath
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
import sys
|
|
||||||
|
|
||||||
|
from cairosvg import svg2png
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from mkdocs.commands.build import DuplicateFilter
|
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 mkdocs.plugins import BasePlugin
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
try:
|
from material.plugins.social.config import SocialConfig
|
||||||
from cairosvg import svg2png
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
dependencies = True
|
|
||||||
except ImportError:
|
|
||||||
dependencies = False
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# 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'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Social plugin
|
# Social plugin
|
||||||
class SocialPlugin(BasePlugin[SocialPluginConfig]):
|
class SocialPlugin(BasePlugin[SocialConfig]):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._executor = concurrent.futures.ThreadPoolExecutor(4)
|
self._executor = concurrent.futures.ThreadPoolExecutor(4)
|
||||||
@ -104,14 +74,6 @@ class SocialPlugin(BasePlugin[SocialPluginConfig]):
|
|||||||
value = self.config.cards_font
|
value = self.config.cards_font
|
||||||
self.config.cards_layout_options["font_family"] = value
|
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
|
# Check if site URL is defined
|
||||||
if not config.site_url:
|
if not config.site_url:
|
||||||
log.warning(
|
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 material.plugins.tags import casefold
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Tags plugin configuration scheme
|
||||||
|
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 collections import defaultdict
|
||||||
from markdown.extensions.toc import slugify
|
from markdown.extensions.toc import slugify
|
||||||
from mkdocs import utils
|
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
|
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 material.plugins.tags import casefold
|
||||||
|
from material.plugins.tags.config import TagsConfig
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# Class
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Tags plugin configuration scheme
|
|
||||||
class TagsPluginConfig(Config):
|
|
||||||
enabled = opt.Type(bool, default = True)
|
|
||||||
|
|
||||||
# Options for tags
|
|
||||||
tags_file = opt.Optional(opt.Type(str))
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Tags plugin
|
# Tags plugin
|
||||||
class TagsPlugin(BasePlugin[TagsPluginConfig]):
|
class TagsPlugin(BasePlugin[TagsConfig]):
|
||||||
supports_multiple_instances = True
|
supports_multiple_instances = True
|
||||||
|
|
||||||
# Initialize plugin
|
# Initialize plugin
|
||||||
@ -166,5 +159,4 @@ class TagsPlugin(BasePlugin[TagsPluginConfig]):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
log = logging.getLogger("mkdocs")
|
log = logging.getLogger("mkdocs.material.tags")
|
||||||
log.addFilter(DuplicateFilter())
|
|
||||||
|
@ -27,11 +27,38 @@ import {
|
|||||||
fromEvent,
|
fromEvent,
|
||||||
map,
|
map,
|
||||||
merge,
|
merge,
|
||||||
|
shareReplay,
|
||||||
startWith
|
startWith
|
||||||
} from "rxjs"
|
} from "rxjs"
|
||||||
|
|
||||||
import { getActiveElement } from "../_"
|
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
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
@ -39,14 +66,6 @@ import { getActiveElement } from "../_"
|
|||||||
/**
|
/**
|
||||||
* Watch element focus
|
* 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
|
* @param el - Element
|
||||||
*
|
*
|
||||||
* @returns Element focus observable
|
* @returns Element focus observable
|
||||||
@ -54,19 +73,9 @@ import { getActiveElement } from "../_"
|
|||||||
export function watchElementFocus(
|
export function watchElementFocus(
|
||||||
el: HTMLElement
|
el: HTMLElement
|
||||||
): Observable<boolean> {
|
): Observable<boolean> {
|
||||||
return merge(
|
return observer$
|
||||||
fromEvent(document.body, "focusin"),
|
|
||||||
fromEvent(document.body, "focusout")
|
|
||||||
)
|
|
||||||
.pipe(
|
.pipe(
|
||||||
debounceTime(1),
|
map(active => el.contains(active)),
|
||||||
map(() => {
|
|
||||||
const active = getActiveElement()
|
|
||||||
return typeof active !== "undefined"
|
|
||||||
? el.contains(active)
|
|
||||||
: false
|
|
||||||
}),
|
|
||||||
startWith(el === getActiveElement()),
|
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ import { h } from "~/utilities"
|
|||||||
* @returns Location hash
|
* @returns Location hash
|
||||||
*/
|
*/
|
||||||
export function getLocationHash(): string {
|
export function getLocationHash(): string {
|
||||||
return location.hash.substring(1)
|
return location.hash.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "iframe-worker/shim"
|
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
Subject,
|
Subject,
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
import "array-flat-polyfill"
|
import "array-flat-polyfill"
|
||||||
import "focus-visible"
|
import "focus-visible"
|
||||||
|
import "iframe-worker/shim"
|
||||||
import "unfetch/polyfill"
|
import "unfetch/polyfill"
|
||||||
import "url-polyfill"
|
import "url-polyfill"
|
||||||
|
|
||||||
|
@ -22,20 +22,26 @@
|
|||||||
|
|
||||||
import { Observable, merge } from "rxjs"
|
import { Observable, merge } from "rxjs"
|
||||||
|
|
||||||
|
import { feature } from "~/_"
|
||||||
import { Viewport, getElements } from "~/browser"
|
import { Viewport, getElements } from "~/browser"
|
||||||
|
|
||||||
import { Component } from "../../_"
|
import { Component } from "../../_"
|
||||||
import { Annotation } from "../annotation"
|
import {
|
||||||
|
Annotation,
|
||||||
|
mountAnnotationBlock
|
||||||
|
} from "../annotation"
|
||||||
import {
|
import {
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
Mermaid,
|
mountCodeBlock
|
||||||
mountCodeBlock,
|
|
||||||
mountMermaid
|
|
||||||
} from "../code"
|
} from "../code"
|
||||||
import {
|
import {
|
||||||
Details,
|
Details,
|
||||||
mountDetails
|
mountDetails
|
||||||
} from "../details"
|
} from "../details"
|
||||||
|
import {
|
||||||
|
Mermaid,
|
||||||
|
mountMermaid
|
||||||
|
} from "../mermaid"
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
mountDataTable
|
mountDataTable
|
||||||
@ -54,11 +60,11 @@ import {
|
|||||||
*/
|
*/
|
||||||
export type Content =
|
export type Content =
|
||||||
| Annotation
|
| Annotation
|
||||||
| ContentTabs
|
|
||||||
| CodeBlock
|
| CodeBlock
|
||||||
| Mermaid
|
| ContentTabs
|
||||||
| DataTable
|
| DataTable
|
||||||
| Details
|
| Details
|
||||||
|
| Mermaid
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Helper types
|
* Helper types
|
||||||
@ -93,6 +99,10 @@ export function mountContent(
|
|||||||
): Observable<Component<Content>> {
|
): Observable<Component<Content>> {
|
||||||
return merge(
|
return merge(
|
||||||
|
|
||||||
|
/* Annotations */
|
||||||
|
...getElements(".annotate:not(.highlight)", el)
|
||||||
|
.map(child => mountAnnotationBlock(child, { target$, print$ })),
|
||||||
|
|
||||||
/* Code blocks */
|
/* Code blocks */
|
||||||
...getElements("pre:not(.mermaid) > code", el)
|
...getElements("pre:not(.mermaid) > code", el)
|
||||||
.map(child => mountCodeBlock(child, { target$, print$ })),
|
.map(child => mountCodeBlock(child, { target$, print$ })),
|
||||||
|
@ -102,7 +102,7 @@ export function watchAnnotation(
|
|||||||
map(([{ x, y }, scroll]): ElementOffset => {
|
map(([{ x, y }, scroll]): ElementOffset => {
|
||||||
const { width, height } = getElementSize(el)
|
const { width, height } = getElementSize(el)
|
||||||
return ({
|
return ({
|
||||||
x: x - scroll.x + width / 2,
|
x: x - scroll.x + width / 2,
|
||||||
y: y - scroll.y + height / 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 "./_"
|
||||||
|
export * from "./block"
|
||||||
export * from "./list"
|
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
|
* @param container - Containing element
|
||||||
*
|
*
|
||||||
* @returns Annotation markers
|
* @returns Annotation markers
|
||||||
*/
|
*/
|
||||||
function findAnnotationMarkers(container: HTMLElement): Text[] {
|
function findMarkers(container: HTMLElement): Text[] {
|
||||||
const markers: Text[] = []
|
const markers: Text[] = []
|
||||||
for (const el of getElements(".c, .c1, .cm", container)) {
|
for (const el of findHosts(container)) {
|
||||||
const nodes: Text[] = []
|
const nodes: Text[] = []
|
||||||
|
|
||||||
/* Find all text nodes in current element */
|
/* Find all text nodes in current element */
|
||||||
@ -141,7 +154,7 @@ export function mountAnnotationList(
|
|||||||
|
|
||||||
/* Find and replace all markers with empty annotations */
|
/* Find and replace all markers with empty annotations */
|
||||||
const annotations = new Map<string, HTMLElement>()
|
const annotations = new Map<string, HTMLElement>()
|
||||||
for (const marker of findAnnotationMarkers(container)) {
|
for (const marker of findMarkers(container)) {
|
||||||
const [, id] = marker.textContent!.match(/\((\d+)\)/)!
|
const [, id] = marker.textContent!.match(/\((\d+)\)/)!
|
||||||
if (getOptionalElement(`:scope > li:nth-child(${id})`, el)) {
|
if (getOptionalElement(`:scope > li:nth-child(${id})`, el)) {
|
||||||
annotations.set(id, renderAnnotation(id, prefix))
|
annotations.set(id, renderAnnotation(id, prefix))
|
||||||
@ -155,7 +168,8 @@ export function mountAnnotationList(
|
|||||||
|
|
||||||
/* Mount component on subscription */
|
/* Mount component on subscription */
|
||||||
return defer(() => {
|
return defer(() => {
|
||||||
const done$ = new Subject()
|
const push$ = new Subject()
|
||||||
|
const done$ = push$.pipe(ignoreElements(), endWith(true))
|
||||||
|
|
||||||
/* Retrieve container pairs for swapping */
|
/* Retrieve container pairs for swapping */
|
||||||
const pairs: [HTMLElement, HTMLElement][] = []
|
const pairs: [HTMLElement, HTMLElement][] = []
|
||||||
@ -166,20 +180,20 @@ export function mountAnnotationList(
|
|||||||
])
|
])
|
||||||
|
|
||||||
/* Handle print mode - see https://bit.ly/3rgPdpt */
|
/* Handle print mode - see https://bit.ly/3rgPdpt */
|
||||||
print$
|
print$.pipe(takeUntil(done$))
|
||||||
.pipe(
|
.subscribe(active => {
|
||||||
takeUntil(done$.pipe(ignoreElements(), endWith(true)))
|
el.hidden = !active
|
||||||
)
|
|
||||||
.subscribe(active => {
|
|
||||||
el.hidden = !active
|
|
||||||
|
|
||||||
/* Show annotations in code block or list (print) */
|
/* Add class to discern list element */
|
||||||
for (const [inner, child] of pairs)
|
el.classList.toggle("md-annotation-list", active)
|
||||||
if (!active)
|
|
||||||
swap(child, inner)
|
/* Show annotations in code block or list (print) */
|
||||||
else
|
for (const [inner, child] of pairs)
|
||||||
swap(inner, child)
|
if (!active)
|
||||||
})
|
swap(child, inner)
|
||||||
|
else
|
||||||
|
swap(inner, child)
|
||||||
|
})
|
||||||
|
|
||||||
/* Create and return component */
|
/* Create and return component */
|
||||||
return merge(...[...annotations]
|
return merge(...[...annotations]
|
||||||
@ -188,7 +202,7 @@ export function mountAnnotationList(
|
|||||||
))
|
))
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
finalize(() => done$.complete()),
|
finalize(() => push$.complete()),
|
||||||
share()
|
share()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -21,4 +21,3 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./_"
|
export * from "./_"
|
||||||
export * from "./mermaid"
|
|
||||||
|
@ -31,7 +31,7 @@ import {
|
|||||||
import { watchScript } from "~/browser"
|
import { watchScript } from "~/browser"
|
||||||
import { h } from "~/utilities"
|
import { h } from "~/utilities"
|
||||||
|
|
||||||
import { Component } from "../../../_"
|
import { Component } from "../../_"
|
||||||
|
|
||||||
import themeCSS from "./index.css"
|
import themeCSS from "./index.css"
|
||||||
|
|
@ -129,7 +129,7 @@ export function mountHeaderTitle(
|
|||||||
})
|
})
|
||||||
|
|
||||||
/* Obtain headline, if any */
|
/* Obtain headline, if any */
|
||||||
const heading = getOptionalElement("article h1")
|
const heading = getOptionalElement(".md-content h1")
|
||||||
if (typeof heading === "undefined")
|
if (typeof heading === "undefined")
|
||||||
return EMPTY
|
return EMPTY
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ interface MountOptions {
|
|||||||
export function watchSidebar(
|
export function watchSidebar(
|
||||||
el: HTMLElement, { viewport$, main$ }: WatchOptions
|
el: HTMLElement, { viewport$, main$ }: WatchOptions
|
||||||
): Observable<Sidebar> {
|
): Observable<Sidebar> {
|
||||||
const parent = el.parentElement!
|
const parent = el.closest<HTMLElement>(".md-grid")!
|
||||||
const adjust =
|
const adjust =
|
||||||
parent.offsetTop -
|
parent.offsetTop -
|
||||||
parent.parentElement!.offsetTop
|
parent.parentElement!.offsetTop
|
||||||
|
@ -110,7 +110,7 @@ export function setupInstantLoading(
|
|||||||
return EMPTY
|
return EMPTY
|
||||||
|
|
||||||
// Skip, as target is not within a link - clicks on non-link elements
|
// Skip, as target is not within a link - clicks on non-link elements
|
||||||
// are also captured, which we need to exclude from processing.
|
// are also captured, which we need to exclude from processing
|
||||||
const el = ev.target.closest("a")
|
const el = ev.target.closest("a")
|
||||||
if (el === null)
|
if (el === null)
|
||||||
return EMPTY
|
return EMPTY
|
||||||
@ -151,7 +151,7 @@ export function setupInstantLoading(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Before fetching for the first time, resolve the absolute favicon position,
|
// Before fetching for the first time, resolve the absolute favicon position,
|
||||||
// as the browser will try to fetch the icon immediately.
|
// as the browser will try to fetch the icon immediately
|
||||||
instant$.pipe(take(1))
|
instant$.pipe(take(1))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
const favicon = getOptionalElement<HTMLLinkElement>("link[rel=icon]")
|
const favicon = getOptionalElement<HTMLLinkElement>("link[rel=icon]")
|
||||||
@ -216,7 +216,7 @@ export function setupInstantLoading(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Initialize the DOM parser, parse the returned HTML, and replace selected
|
// Initialize the DOM parser, parse the returned HTML, and replace selected
|
||||||
// meta tags and components before handing control down to the application.
|
// meta tags and components before handing control down to the application
|
||||||
const dom = new DOMParser()
|
const dom = new DOMParser()
|
||||||
const document$ = response$
|
const document$ = response$
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -253,7 +253,7 @@ export function setupInstantLoading(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// After meta tags and components were replaced, re-evaluate scripts
|
// After meta tags and components were replaced, re-evaluate scripts
|
||||||
// that were provided by the author as part of Markdown files.
|
// that were provided by the author as part of Markdown files
|
||||||
const container = getComponentElement("container")
|
const container = getComponentElement("container")
|
||||||
return concat(getElements("script", container))
|
return concat(getElements("script", container))
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -284,7 +284,7 @@ export function setupInstantLoading(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Intercept popstate events, e.g. when using the browser's back and forward
|
// Intercept popstate events, e.g. when using the browser's back and forward
|
||||||
// buttons, and emit new location for fetching and parsing.
|
// buttons, and emit new location for fetching and parsing
|
||||||
const popstate$ = fromEvent<PopStateEvent>(window, "popstate")
|
const popstate$ = fromEvent<PopStateEvent>(window, "popstate")
|
||||||
popstate$.pipe(map(getLocation))
|
popstate$.pipe(map(getLocation))
|
||||||
.subscribe(location$)
|
.subscribe(location$)
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
SearchQueryTerms,
|
SearchQueryTerms,
|
||||||
getSearchQueryTerms,
|
getSearchQueryTerms,
|
||||||
parseSearchQuery,
|
parseSearchQuery,
|
||||||
|
segment,
|
||||||
transformSearchQuery
|
transformSearchQuery
|
||||||
} from "../query"
|
} from "../query"
|
||||||
|
|
||||||
@ -204,6 +205,14 @@ export class Search {
|
|||||||
* @returns Search result
|
* @returns Search result
|
||||||
*/
|
*/
|
||||||
public search(query: string): SearchResult {
|
public search(query: string): SearchResult {
|
||||||
|
|
||||||
|
// Experimental Chinese segmentation
|
||||||
|
query = query.replace(/\p{sc=Han}+/gu, value => {
|
||||||
|
return [...segment(value, this.index.invertedIndex)]
|
||||||
|
.join("* ")
|
||||||
|
})
|
||||||
|
|
||||||
|
// @todo: move segmenter (above) into transformSearchQuery
|
||||||
query = transformSearchQuery(query)
|
query = transformSearchQuery(query)
|
||||||
if (!query)
|
if (!query)
|
||||||
return { items: [] }
|
return { items: [] }
|
||||||
|
@ -21,4 +21,5 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./_"
|
export * from "./_"
|
||||||
|
export * from "./segment"
|
||||||
export * from "./transform"
|
export * from "./transform"
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Segment a search query using the inverted index
|
||||||
|
*
|
||||||
|
* This function implements a clever approach to text segmentation for Asian
|
||||||
|
* languages, as it used the information already available in the search index.
|
||||||
|
* The idea is to greedily segment the search query based on the tokens that are
|
||||||
|
* already part of the index, as described in the linked issue.
|
||||||
|
*
|
||||||
|
* @see https://bit.ly/3lwjrk7 - GitHub issue
|
||||||
|
*
|
||||||
|
* @param query - Query value
|
||||||
|
* @param index - Inverted index
|
||||||
|
*
|
||||||
|
* @returns Segmented query value
|
||||||
|
*/
|
||||||
|
export function segment(
|
||||||
|
query: string, index: object
|
||||||
|
): Iterable<string> {
|
||||||
|
const segments = new Set<string>()
|
||||||
|
|
||||||
|
/* Segment search query */
|
||||||
|
const wordcuts = new Uint16Array(query.length)
|
||||||
|
for (let i = 0; i < query.length; i++)
|
||||||
|
for (let j = i + 1; j < query.length; j++) {
|
||||||
|
const value = query.slice(i, j)
|
||||||
|
if (value in index)
|
||||||
|
wordcuts[i] = j - i
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compute longest matches with minimum overlap */
|
||||||
|
const stack = [0]
|
||||||
|
for (let s = stack.length; s > 0;) {
|
||||||
|
const p = stack[--s]
|
||||||
|
for (let q = 1; q < wordcuts[p]; q++)
|
||||||
|
if (wordcuts[p + q] > wordcuts[p] - q) {
|
||||||
|
segments.add(query.slice(p, p + q))
|
||||||
|
stack[s++] = p + q
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Continue at end of query string */
|
||||||
|
const q = p + wordcuts[p]
|
||||||
|
if (wordcuts[q] && q < query.length - 1)
|
||||||
|
stack[s++] = q
|
||||||
|
|
||||||
|
/* Add current segment */
|
||||||
|
segments.add(query.slice(p, q))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo fix this case in the code block above, this is a hotfix
|
||||||
|
if (segments.has(""))
|
||||||
|
return new Set([query])
|
||||||
|
|
||||||
|
/* Return segmented query value */
|
||||||
|
return segments
|
||||||
|
}
|
@ -41,6 +41,7 @@
|
|||||||
@import "main/icons";
|
@import "main/icons";
|
||||||
@import "main/typeset";
|
@import "main/typeset";
|
||||||
|
|
||||||
|
@import "main/components/author";
|
||||||
@import "main/components/banner";
|
@import "main/components/banner";
|
||||||
@import "main/components/base";
|
@import "main/components/base";
|
||||||
@import "main/components/clipboard";
|
@import "main/components/clipboard";
|
||||||
@ -51,11 +52,15 @@
|
|||||||
@import "main/components/footer";
|
@import "main/components/footer";
|
||||||
@import "main/components/form";
|
@import "main/components/form";
|
||||||
@import "main/components/header";
|
@import "main/components/header";
|
||||||
|
@import "main/components/meta";
|
||||||
@import "main/components/nav";
|
@import "main/components/nav";
|
||||||
|
@import "main/components/pagination";
|
||||||
|
@import "main/components/post";
|
||||||
@import "main/components/search";
|
@import "main/components/search";
|
||||||
@import "main/components/select";
|
@import "main/components/select";
|
||||||
@import "main/components/sidebar";
|
@import "main/components/sidebar";
|
||||||
@import "main/components/source";
|
@import "main/components/source";
|
||||||
|
@import "main/components/status";
|
||||||
@import "main/components/tabs";
|
@import "main/components/tabs";
|
||||||
@import "main/components/tag";
|
@import "main/components/tag";
|
||||||
@import "main/components/tooltip";
|
@import "main/components/tooltip";
|
||||||
|
86
src/assets/stylesheets/main/components/_author.scss
Normal file
86
src/assets/stylesheets/main/components/_author.scss
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
////
|
||||||
|
/// 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
|
||||||
|
////
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Rules
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Scoped in typesetted content to match specificity of regular content
|
||||||
|
.md-typeset {
|
||||||
|
|
||||||
|
// Author, i.e., GitHub user
|
||||||
|
.md-author {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: px2rem(32px);
|
||||||
|
height: px2rem(32px);
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
color 125ms,
|
||||||
|
transform 125ms;
|
||||||
|
|
||||||
|
// Author image
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// More authors
|
||||||
|
&--more {
|
||||||
|
font-size: px2rem(12px);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: px2rem(32px);
|
||||||
|
color: var(--md-default-fg-color--lighter);
|
||||||
|
text-align: center;
|
||||||
|
background: var(--md-default-fg-color--lightest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enlarge image
|
||||||
|
&--long {
|
||||||
|
width: px2rem(48px);
|
||||||
|
height: px2rem(48px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author link
|
||||||
|
a.md-author {
|
||||||
|
transform: scale(1);
|
||||||
|
|
||||||
|
// Author image
|
||||||
|
img {
|
||||||
|
filter: grayscale(100%) opacity(75%);
|
||||||
|
transition: filter 125ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author on focus/hover
|
||||||
|
&:is(:focus, :hover) {
|
||||||
|
z-index: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
|
||||||
|
// Author image
|
||||||
|
img {
|
||||||
|
filter: grayscale(0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
src/assets/stylesheets/main/components/_meta.scss
Normal file
67
src/assets/stylesheets/main/components/_meta.scss
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
////
|
||||||
|
/// 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
|
||||||
|
////
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Rules
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
.md-meta {
|
||||||
|
font-size: px2rem(14px);
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--md-default-fg-color--light);
|
||||||
|
|
||||||
|
// Metadata list
|
||||||
|
&__list {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata item separator
|
||||||
|
&__item:not(:last-child)::after {
|
||||||
|
margin-inline: px2rem(4px);
|
||||||
|
content: "·";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata link
|
||||||
|
&__link {
|
||||||
|
color: var(--md-typeset-a-color);
|
||||||
|
|
||||||
|
// Metadata link on focus/hover
|
||||||
|
&:is(:focus, :hover) {
|
||||||
|
color: var(--md-accent-fg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draft
|
||||||
|
.md-draft {
|
||||||
|
display: inline-block;
|
||||||
|
padding-inline: px2em(8px, 14px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: hsla(255, 100%, 100%);
|
||||||
|
background-color: $clr-red-a400;
|
||||||
|
border-radius: px2em(2px);
|
||||||
|
}
|
@ -93,12 +93,8 @@
|
|||||||
// Navigation link
|
// Navigation link
|
||||||
&__link {
|
&__link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 0.625em;
|
margin-top: 0.625em;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 125ms;
|
transition: color 125ms;
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
|
|
||||||
@ -112,14 +108,22 @@
|
|||||||
color: var(--md-typeset-a-color);
|
color: var(--md-typeset-a-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stretch section index link to full width
|
// Navigation link icon
|
||||||
.md-nav__item &--index [href] {
|
svg {
|
||||||
width: 100%;
|
flex-shrink: 0;
|
||||||
|
height: 1.3em;
|
||||||
|
fill: currentcolor;
|
||||||
|
|
||||||
|
// Adjust spacing of next child
|
||||||
|
+ * {
|
||||||
|
margin-inline-start: px2rem(8px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation link on focus/hover
|
// Navigation link on focus/hover
|
||||||
&:is(:focus, :hover) {
|
&:not(.md-nav__container):is(:focus, :hover) {
|
||||||
color: var(--md-accent-fg-color);
|
color: var(--md-accent-fg-color);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show outline for keyboard devices
|
// Show outline for keyboard devices
|
||||||
@ -146,11 +150,15 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Navigation link children (for section indexes)
|
// Navigation container (for section index pages)
|
||||||
> * {
|
&__container > .md-nav__link {
|
||||||
display: flex;
|
margin-top: 0;
|
||||||
cursor: pointer;
|
|
||||||
|
// Stretch first child
|
||||||
|
&:first-child {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,6 +291,16 @@
|
|||||||
padding: px2rem(12px) px2rem(16px);
|
padding: px2rem(12px) px2rem(16px);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
|
// Navigation link icon
|
||||||
|
svg {
|
||||||
|
margin-top: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust spacing on nested link
|
||||||
|
> .md-nav__link {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Navigation icon
|
// Navigation icon
|
||||||
.md-nav__icon {
|
.md-nav__icon {
|
||||||
width: px2rem(24px);
|
width: px2rem(24px);
|
||||||
@ -515,16 +533,15 @@
|
|||||||
// Show navigation link as title
|
// Show navigation link as title
|
||||||
> .md-nav__link {
|
> .md-nav__link {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
// Make labels discernable from links
|
// Make labels discernable from links
|
||||||
&[for] {
|
&[for] {
|
||||||
color: var(--md-default-fg-color--light);
|
color: var(--md-default-fg-color--light);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make navigation link clickable
|
// Omit clicks if not a section index page
|
||||||
&--index [href] {
|
&:not(.md-nav__container) {
|
||||||
pointer-events: initial;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide naviation icon
|
// Hide naviation icon
|
||||||
@ -613,8 +630,8 @@
|
|||||||
background: var(--md-default-bg-color);
|
background: var(--md-default-bg-color);
|
||||||
box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color);
|
box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color);
|
||||||
|
|
||||||
// Non-index section should not be clickable
|
// Omit clicks if not a section index page
|
||||||
&:not(.md-nav__link--index) {
|
&:not(.md-nav__container) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
85
src/assets/stylesheets/main/components/_pagination.scss
Normal file
85
src/assets/stylesheets/main/components/_pagination.scss
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
|
||||||
|
////
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Rules
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
.md-pagination {
|
||||||
|
display: flex;
|
||||||
|
gap: px2rem(8px);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: px2rem(16px);
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
// Pagination item
|
||||||
|
> * {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: px2rem(36px);
|
||||||
|
height: px2rem(36px);
|
||||||
|
text-align: center;
|
||||||
|
border-radius: px2rem(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active pagination item
|
||||||
|
&__current {
|
||||||
|
color: var(--md-default-fg-color--light);
|
||||||
|
background-color: var(--md-default-fg-color--lightest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination link
|
||||||
|
&__link {
|
||||||
|
transition:
|
||||||
|
color 125ms,
|
||||||
|
background-color 125ms;
|
||||||
|
|
||||||
|
// Pagination link on focus/hover
|
||||||
|
&:is(:focus, :hover) {
|
||||||
|
color: var(--md-accent-fg-color);
|
||||||
|
background-color: var(--md-accent-fg-color--transparent);
|
||||||
|
|
||||||
|
// Pagination icon
|
||||||
|
svg {
|
||||||
|
color: var(--md-accent-fg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show outline for keyboard devices
|
||||||
|
&.focus-visible {
|
||||||
|
outline-color: var(--md-accent-fg-color);
|
||||||
|
outline-offset: px2rem(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination icon
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
width: px2rem(24px);
|
||||||
|
max-height: 100%;
|
||||||
|
color: var(--md-default-fg-color--lighter);
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
161
src/assets/stylesheets/main/components/_post.scss
Normal file
161
src/assets/stylesheets/main/components/_post.scss
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
////
|
||||||
|
/// 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
|
||||||
|
////
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Rules
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Post
|
||||||
|
.md-post {
|
||||||
|
|
||||||
|
// Post backlink
|
||||||
|
&__back {
|
||||||
|
padding-bottom: px2rem(24px);
|
||||||
|
margin-bottom: px2rem(24px);
|
||||||
|
border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest);
|
||||||
|
|
||||||
|
// [tablet -]: Hide post backlink
|
||||||
|
@include break-to-device(tablet) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for right-to-left languages
|
||||||
|
[dir="rtl"] & {
|
||||||
|
|
||||||
|
// Flip icon vertically
|
||||||
|
svg {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post authors
|
||||||
|
&__authors {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: px2rem(12px);
|
||||||
|
margin: 0 px2rem(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post metadata
|
||||||
|
.md-post__meta {
|
||||||
|
|
||||||
|
// Navigation link
|
||||||
|
a {
|
||||||
|
transition: color 125ms;
|
||||||
|
|
||||||
|
// Navigation link on focus/hover
|
||||||
|
&:is(:focus, :hover) {
|
||||||
|
color: var(--md-accent-fg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post excerpt
|
||||||
|
&--excerpt {
|
||||||
|
margin-bottom: px2rem(64px);
|
||||||
|
|
||||||
|
// Post excerpt header
|
||||||
|
.md-post__header {
|
||||||
|
display: flex;
|
||||||
|
gap: px2rem(12px);
|
||||||
|
align-items: center;
|
||||||
|
min-height: px2rem(32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post excerpt authors
|
||||||
|
.md-post__authors {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: px2rem(4px);
|
||||||
|
align-items: center;
|
||||||
|
min-height: px2rem(48px);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post excerpt metadata
|
||||||
|
.md-post__meta .md-meta__list {
|
||||||
|
margin-inline-end: px2rem(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post excerpt content
|
||||||
|
.md-post__content > :first-child {
|
||||||
|
--md-scroll-margin: #{px2rem(120px)};
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust spacing for navigation
|
||||||
|
> .md-nav:first-child > .md-nav__list,
|
||||||
|
> .md-nav--secondary {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Post author profile
|
||||||
|
.md-profile {
|
||||||
|
display: flex;
|
||||||
|
gap: px2rem(12px);
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
font-size: px2rem(14px);
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
// Post author description
|
||||||
|
&__description {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Content area for post
|
||||||
|
.md-content--post {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
// [tablet -]: Switch to inverted column layout
|
||||||
|
@include break-to-device(tablet) {
|
||||||
|
flex-flow: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content wrapper
|
||||||
|
> .md-content__inner {
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
// [screen +]: Adjust spacing between content area and sidebars
|
||||||
|
@include break-from-device(screen) {
|
||||||
|
margin-inline-start: px2rem(24px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sidebar for post
|
||||||
|
.md-sidebar.md-sidebar--post {
|
||||||
|
|
||||||
|
// [tablet -]: Adjust spacing
|
||||||
|
@include break-to-device(tablet) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
74
src/assets/stylesheets/main/components/_status.scss
Normal file
74
src/assets/stylesheets/main/components/_status.scss
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
////
|
||||||
|
/// 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
|
||||||
|
////
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Rules
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Status variables
|
||||||
|
:root {
|
||||||
|
--md-status: svg-load("material/information-outline.svg");
|
||||||
|
--md-status--new: svg-load("material/alert-decagram.svg");
|
||||||
|
--md-status--deprecated: svg-load("material/trash-can.svg");
|
||||||
|
--md-status--encrypted: svg-load("material/shield-lock.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Status
|
||||||
|
.md-status {
|
||||||
|
margin-left: px2rem(4px);
|
||||||
|
|
||||||
|
// Status icon
|
||||||
|
&::after {
|
||||||
|
display: inline-block;
|
||||||
|
width: px2em(18px);
|
||||||
|
height: px2em(18px);
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
content: "";
|
||||||
|
background-color: var(--md-default-fg-color--light);
|
||||||
|
mask-image: var(--md-status);
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status icon on hover
|
||||||
|
&:hover::after {
|
||||||
|
background-color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status: new
|
||||||
|
&--new::after {
|
||||||
|
mask-image: var(--md-status--new);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status: deprecated
|
||||||
|
&--deprecated::after {
|
||||||
|
mask-image: var(--md-status--deprecated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status: encrypted
|
||||||
|
&--encrypted::after {
|
||||||
|
mask-image: var(--md-status--encrypted);
|
||||||
|
}
|
||||||
|
}
|
@ -53,6 +53,7 @@
|
|||||||
|
|
||||||
// Navigation tabs list
|
// Navigation tabs list
|
||||||
&__list {
|
&__list {
|
||||||
|
display: flex;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-inline-start: px2rem(4px);
|
margin-inline-start: px2rem(4px);
|
||||||
@ -74,7 +75,6 @@
|
|||||||
|
|
||||||
// Navigation tabs item
|
// Navigation tabs item
|
||||||
&__item {
|
&__item {
|
||||||
display: inline-block;
|
|
||||||
height: px2rem(48px);
|
height: px2rem(48px);
|
||||||
padding-inline: px2rem(12px);
|
padding-inline: px2rem(12px);
|
||||||
}
|
}
|
||||||
@ -82,7 +82,7 @@
|
|||||||
// Navigation tabs link - could be defined as block elements and aligned via
|
// Navigation tabs link - could be defined as block elements and aligned via
|
||||||
// line height, but this would imply more repaints when scrolling
|
// line height, but this would imply more repaints when scrolling
|
||||||
&__link {
|
&__link {
|
||||||
display: block;
|
display: flex;
|
||||||
margin-top: px2rem(16px);
|
margin-top: px2rem(16px);
|
||||||
font-size: px2rem(14px);
|
font-size: px2rem(14px);
|
||||||
outline-color: var(--md-accent-fg-color);
|
outline-color: var(--md-accent-fg-color);
|
||||||
@ -101,6 +101,13 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation tabs link icon
|
||||||
|
svg {
|
||||||
|
height: 1.3em;
|
||||||
|
margin-inline-end: px2rem(8px);
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
// Delay transitions by a small amount
|
// Delay transitions by a small amount
|
||||||
@for $i from 2 through 16 {
|
@for $i from 2 through 16 {
|
||||||
.md-tabs__item:nth-child(#{$i}) & {
|
.md-tabs__item:nth-child(#{$i}) & {
|
||||||
|
@ -27,17 +27,14 @@
|
|||||||
// Continuous pulse animation
|
// Continuous pulse animation
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0 0 var(--md-default-fg-color--lightest);
|
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
box-shadow: 0 0 0 px2em(10px) transparent;
|
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 0 0 transparent;
|
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,6 +45,8 @@
|
|||||||
|
|
||||||
// Tooltip variables
|
// Tooltip variables
|
||||||
:root {
|
:root {
|
||||||
|
--md-annotation-bg-icon: svg-load("material/circle.svg");
|
||||||
|
--md-annotation-icon: svg-load("material/plus-circle.svg");
|
||||||
--md-tooltip-width: #{px2rem(400px)};
|
--md-tooltip-width: #{px2rem(400px)};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +123,7 @@
|
|||||||
.md-annotation {
|
.md-annotation {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
vertical-align: text-bottom;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
// Adjust for right-to-left languages
|
// Adjust for right-to-left languages
|
||||||
@ -131,125 +131,156 @@
|
|||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Annotation index in code block
|
||||||
|
code & {
|
||||||
|
font-family: var(--md-code-font-family);
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
// Annotation is not hidden (e.g. when copying)
|
// Annotation is not hidden (e.g. when copying)
|
||||||
&:not([hidden]) {
|
&:not([hidden]) {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
// Hack: ensure that the line height doesn't exceed the line height of the
|
// Hack: ensure that the line height doesn't exceed the line height of the
|
||||||
// hosting line, because it will lead to dancing pixels.
|
// hosting line, because it will lead to dancing pixels.
|
||||||
line-height: 1.325;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotation index
|
// Annotation index
|
||||||
&__index {
|
&__index {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
margin: 0 1ch;
|
display: inline-block;
|
||||||
font-family: var(--md-code-font-family);
|
margin-inline: 0.4ch;
|
||||||
font-size: px2em(13.6px, 16px);
|
vertical-align: text-top;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
// Hack: increase specificity to override default for anchors
|
// Hack: increase specificity to override default for anchors in typesetted
|
||||||
|
// content, because transitions are defined on anchor elements
|
||||||
.md-annotation & {
|
.md-annotation & {
|
||||||
color: hsla(0, 0%, 100%, 1);
|
|
||||||
transition: z-index 250ms;
|
transition: z-index 250ms;
|
||||||
|
|
||||||
// Text link on focus/hover
|
|
||||||
&:is(:focus, :hover) {
|
|
||||||
color: hsla(0, 0%, 100%, 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotation marker – the marker must be positioned absolutely behind
|
// [screen]: Render annotation markers as icons
|
||||||
// the index, because it shouldn't impact the rendering of a code block.
|
@media screen {
|
||||||
// Otherwise, small rounding differences in browsers can sometimes mess up
|
width: 2.2ch;
|
||||||
// alignment of text following an annotation.
|
|
||||||
&::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: px2em(-2px);
|
|
||||||
z-index: -1;
|
|
||||||
// Hack: the first property is used as a fallback for older browsers
|
|
||||||
// which don't support the min/max/clamp math functions.
|
|
||||||
width: calc(100% + 1.2ch);
|
|
||||||
width: max(2.2ch, 100% + 1.2ch);
|
|
||||||
height: 2.2ch;
|
|
||||||
padding: 0 0.4ch;
|
|
||||||
margin: 0 -0.4ch;
|
|
||||||
content: "";
|
|
||||||
background-color: var(--md-default-fg-color--lighter);
|
|
||||||
border-radius: 2ch;
|
|
||||||
transition:
|
|
||||||
color 250ms,
|
|
||||||
background-color 250ms;
|
|
||||||
|
|
||||||
// [reduced motion]: Disable animation
|
// Annotation is visible
|
||||||
@media not all and (prefers-reduced-motion) {
|
[data-md-visible] > & {
|
||||||
|
animation: pulse 2000ms infinite;
|
||||||
// Annotation marker is visible
|
|
||||||
[data-md-visible] > & {
|
|
||||||
animation: pulse 2000ms infinite;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotation marker for active tooltip
|
// Annotation marker background
|
||||||
.md-tooltip--active + & {
|
&::before {
|
||||||
transition:
|
position: absolute;
|
||||||
color 250ms,
|
top: -0.1ch;
|
||||||
background-color 250ms;
|
z-index: -1;
|
||||||
animation: none;
|
width: 2.2ch;
|
||||||
|
height: 2.2ch;
|
||||||
|
content: "";
|
||||||
|
background: var(--md-default-bg-color);
|
||||||
|
mask-image: var(--md-annotation-bg-icon);
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Annotation index in code block
|
// Annotation marker – the marker must be positioned absolutely behind
|
||||||
code & {
|
// the index, because it shouldn't impact the rendering of a code block.
|
||||||
font-family: var(--md-code-font-family);
|
// Otherwise, small rounding differences in browsers can sometimes mess up
|
||||||
font-size: inherit;
|
// alignment of text following an annotation.
|
||||||
}
|
|
||||||
|
|
||||||
// Annotation index for active tooltip or on hover
|
|
||||||
:is(.md-tooltip--active + &, :hover > &) {
|
|
||||||
color: var(--md-accent-bg-color);
|
|
||||||
|
|
||||||
// Annotation marker
|
|
||||||
&::after {
|
&::after {
|
||||||
background-color: var(--md-accent-fg-color);
|
position: absolute;
|
||||||
|
top: -0.1ch;
|
||||||
|
z-index: -1;
|
||||||
|
width: 2.2ch;
|
||||||
|
height: 2.2ch;
|
||||||
|
content: "";
|
||||||
|
background-color: var(--md-default-fg-color--lighter);
|
||||||
|
transition:
|
||||||
|
background-color 250ms,
|
||||||
|
transform 250ms;
|
||||||
|
// Hack: promote to own layer to reduce jitter
|
||||||
|
transform: scale(1.0001);
|
||||||
|
mask-image: var(--md-annotation-icon);
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
|
||||||
|
// Annotation marker for active tooltip
|
||||||
|
.md-tooltip--active + & {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotation marker for active tooltip or on hover
|
||||||
|
:is(.md-tooltip--active + &, :hover > &) {
|
||||||
|
background-color: var(--md-accent-fg-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotation index for active tooltip
|
// Annotation index for active tooltip
|
||||||
.md-tooltip--active + & {
|
.md-tooltip--active + & {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
transition: none;
|
transition-duration: 0ms;
|
||||||
animation: none;
|
animation-play-state: paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotation marker
|
// Annotation marker
|
||||||
[data-md-annotation-id] {
|
[data-md-annotation-id] {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 90%;
|
|
||||||
|
|
||||||
// Annotation marker content
|
// [print]: Render annotation markers as numbers
|
||||||
&::before {
|
@media print {
|
||||||
display: inline-block;
|
padding: 0 0.6ch;
|
||||||
padding-bottom: 0.1em;
|
font-weight: 700;
|
||||||
vertical-align: 0.065em;
|
color: var(--md-default-bg-color);
|
||||||
content: attr(data-md-annotation-id);
|
white-space: nowrap;
|
||||||
transition: transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1);
|
background: var(--md-default-fg-color--lighter);
|
||||||
transform: scale(1.15);
|
border-radius: 2ch;
|
||||||
|
|
||||||
// [not print]: if we're not in print mode, show a `+` sign instead of
|
// Annotation marker content
|
||||||
// the original numbers, as context is already given by the position.
|
&::after {
|
||||||
@media not print {
|
content: attr(data-md-annotation-id);
|
||||||
content: "+";
|
|
||||||
|
|
||||||
// Annotation marker content on focus
|
|
||||||
:focus-within > & {
|
|
||||||
transform: scale(1.25) rotate(45deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Scoped in typesetted content to match specificity of regular content
|
||||||
|
.md-typeset {
|
||||||
|
|
||||||
|
// Annotation list
|
||||||
|
.md-annotation-list {
|
||||||
|
list-style: none;
|
||||||
|
counter-reset: xxx;
|
||||||
|
|
||||||
|
// Annotation list item
|
||||||
|
li {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// Annotation list marker
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: px2em(4px);
|
||||||
|
inset-inline-start: px2em(-34px);
|
||||||
|
min-width: 2ch;
|
||||||
|
height: 2ch;
|
||||||
|
padding: 0 0.6ch;
|
||||||
|
font-size: px2em(14.2px);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--md-default-bg-color);
|
||||||
|
text-align: center;
|
||||||
|
content: counter(xxx);
|
||||||
|
counter-increment: xxx;
|
||||||
|
background: var(--md-default-fg-color--lighter);
|
||||||
|
border-radius: 2ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -60,6 +60,22 @@
|
|||||||
<link rel="next" href="{{ page.next_page.url | url }}" />
|
<link rel="next" href="{{ page.next_page.url | url }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- RSS feed -->
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="{{ config.theme.favicon | url }}" />
|
<link rel="icon" href="{{ config.theme.favicon | url }}" />
|
||||||
|
|
||||||
|
41
src/blog-archive.html
Normal file
41
src/blog-archive.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% extends "main.html" %}
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
{% block container %}
|
||||||
|
<div class="md-content" data-md-component="content">
|
||||||
|
<div class="md-content__inner">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="md-typeset">
|
||||||
|
{{ page.content }}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Posts -->
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include "partials/post.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
41
src/blog-category.html
Normal file
41
src/blog-category.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% extends "main.html" %}
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
{% block container %}
|
||||||
|
<div class="md-content" data-md-component="content">
|
||||||
|
<div class="md-content__inner">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="md-typeset">
|
||||||
|
{{ page.content }}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Posts -->
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include "partials/post.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
142
src/blog-post.html
Normal file
142
src/blog-post.html
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% extends "main.html" %}
|
||||||
|
|
||||||
|
{% import "partials/nav-item.html" as item with context %}
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
{% block container %}
|
||||||
|
<div class="md-content md-content--post" data-md-component="content">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<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">
|
||||||
|
|
||||||
|
<!-- Back to overview link -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Page authors -->
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<!-- Page metadata -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Page date -->
|
||||||
|
<li class="md-nav__item">
|
||||||
|
<div class="md-nav__link">
|
||||||
|
{% include ".icons/material/calendar.svg" %}
|
||||||
|
<time datetime="{{ page.meta.date }}" class="md-ellipsis">
|
||||||
|
{{- page.meta.date_format -}}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Page categories -->
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<!-- Page readtime -->
|
||||||
|
{% if page.meta.readtime %}
|
||||||
|
{% set time = page.meta.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>
|
||||||
|
|
||||||
|
<!-- Table of contents, if integrated -->
|
||||||
|
{% if "toc.integrate" in features %}
|
||||||
|
{% include "partials/toc.html" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
<article class="md-content__inner md-typeset">
|
||||||
|
{% block content %}
|
||||||
|
{% include "partials/content.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
46
src/blog.html
Normal file
46
src/blog.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% extends "main.html" %}
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
{% block container %}
|
||||||
|
<div class="md-content" data-md-component="content">
|
||||||
|
<div class="md-content__inner">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="md-typeset">
|
||||||
|
{{ page.content }}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Posts -->
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include "partials/post.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% block pagination %}
|
||||||
|
{% include "partials/pagination.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -25,4 +25,4 @@
|
|||||||
{% import "partials/languages/en.html" as fallback %}
|
{% import "partials/languages/en.html" as fallback %}
|
||||||
|
|
||||||
<!-- Re-export translations -->
|
<!-- Re-export translations -->
|
||||||
{% macro t(key) %}{{ lang.t(key) or fallback.t(key) }}{% endmacro %}
|
{% macro t(key) %}{{ lang.t(key) or fallback.t(key) or key }}{% endmacro %}
|
||||||
|
@ -46,9 +46,8 @@
|
|||||||
"header": "頁首",
|
"header": "頁首",
|
||||||
"meta.comments": "評論",
|
"meta.comments": "評論",
|
||||||
"meta.source": "來源",
|
"meta.source": "來源",
|
||||||
"search.config.lang": "ja",
|
|
||||||
"search.config.pipeline": "stemmer",
|
"search.config.pipeline": "stemmer",
|
||||||
"search.config.separator": "[\\s\\-,。]+",
|
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||||
"nav": "導航",
|
"nav": "導航",
|
||||||
"readtime.one": "需要 1 分鐘閲讀",
|
"readtime.one": "需要 1 分鐘閲讀",
|
||||||
"readtime.other": "需要 # 分鐘閲讀",
|
"readtime.other": "需要 # 分鐘閲讀",
|
||||||
|
@ -52,9 +52,8 @@
|
|||||||
"rss.created": "RSS 訂閱",
|
"rss.created": "RSS 訂閱",
|
||||||
"rss.updated": "RSS 訂閱內容已更新",
|
"rss.updated": "RSS 訂閱內容已更新",
|
||||||
"search": "搜尋",
|
"search": "搜尋",
|
||||||
"search.config.lang": "ja",
|
|
||||||
"search.config.pipeline": "stemmer",
|
"search.config.pipeline": "stemmer",
|
||||||
"search.config.separator": "[\\s\\- 、。,.?;]+",
|
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||||
"search.placeholder": "搜尋",
|
"search.placeholder": "搜尋",
|
||||||
"search.share": "分享",
|
"search.share": "分享",
|
||||||
"search.reset": "清除",
|
"search.reset": "清除",
|
||||||
|
@ -52,9 +52,8 @@
|
|||||||
"rss.created": "RSS 订阅",
|
"rss.created": "RSS 订阅",
|
||||||
"rss.updated": "已更新内容的 RSS 订阅",
|
"rss.updated": "已更新内容的 RSS 订阅",
|
||||||
"search": "查找",
|
"search": "查找",
|
||||||
"search.config.lang": "ja",
|
|
||||||
"search.config.pipeline": "stemmer",
|
"search.config.pipeline": "stemmer",
|
||||||
"search.config.separator": "[\\s\\-,。]+",
|
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||||
"search.placeholder": "搜索",
|
"search.placeholder": "搜索",
|
||||||
"search.share": "分享",
|
"search.share": "分享",
|
||||||
"search.reset": "清空当前内容",
|
"search.reset": "清空当前内容",
|
||||||
|
@ -20,103 +20,177 @@
|
|||||||
IN THE SOFTWARE.
|
IN THE SOFTWARE.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Wrap everything with a macro to reduce file roundtrips (see #2213) -->
|
<!-- Render navigation link status -->
|
||||||
|
{% macro render_status(nav_item, type) %}
|
||||||
|
{% set class = "md-status md-status--" ~ type %}
|
||||||
|
|
||||||
|
<!-- Render icon with title (or tooltip), if given -->
|
||||||
|
{% if config.extra.status and config.extra.status[type] %}
|
||||||
|
<span
|
||||||
|
class="{{ class }}"
|
||||||
|
title="{{ config.extra.status[type] }}"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Render icon only -->
|
||||||
|
{% else %}
|
||||||
|
<span class="{{ class }}"></span>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
<!-- Render navigation link content -->
|
||||||
|
{% macro render_content(nav_item, ref = nav_item) %}
|
||||||
|
|
||||||
|
<!-- Navigation link icon -->
|
||||||
|
{% if nav_item.is_page and nav_item.meta.icon %}
|
||||||
|
{% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Navigation link title -->
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
{{ nav_item.title }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Navigation link status -->
|
||||||
|
{% if nav_item.is_page and nav_item.meta.status %}
|
||||||
|
{{ render_status(nav_item, nav_item.meta.status) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
<!-- Render navigation item (pruned) -->
|
||||||
|
{% macro render_pruned(nav_item, ref = nav_item) %}
|
||||||
|
{% set first = nav_item.children | first %}
|
||||||
|
|
||||||
|
<!-- Recurse, if the first item has further nested items -->
|
||||||
|
{% if first and first.children %}
|
||||||
|
{{ render_pruned(first, ref) }}
|
||||||
|
|
||||||
|
<!-- Navigation link -->
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ first.url | url }}" class="md-nav__link">
|
||||||
|
{{ render_content(first, ref) }}
|
||||||
|
|
||||||
|
<!-- Only render toggle if there's at least one more page -->
|
||||||
|
{% if nav_item.children | length > 1 %}
|
||||||
|
<span class="md-nav__icon md-icon"></span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
<!-- Render navigation item -->
|
||||||
{% macro render(nav_item, path, level) %}
|
{% macro render(nav_item, path, level) %}
|
||||||
|
|
||||||
<!-- Determine class according to state -->
|
<!-- Determine base classes -->
|
||||||
{% set class = "md-nav__item" %}
|
{% set class = "md-nav__item" %}
|
||||||
{% if nav_item.active %}
|
{% if nav_item.active %}
|
||||||
{% set class = class ~ " md-nav__item--active" %}
|
{% set class = class ~ " md-nav__item--active" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Main navigation item with nested items -->
|
<!-- Navigation item with nested items -->
|
||||||
{% if nav_item.children %}
|
{% if nav_item.children %}
|
||||||
|
|
||||||
|
<!-- Determine all nested items that are index pages -->
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
<!-- Determine whether to render item as a section -->
|
<!-- Determine whether to render item as a section -->
|
||||||
{% if "navigation.sections" in features and level == 1 + (
|
{% if "navigation.sections" in features and level == 1 + (
|
||||||
"navigation.tabs" in features
|
"navigation.tabs" in features
|
||||||
) %}
|
) %}
|
||||||
{% set class = class ~ " md-nav__item--section" %}
|
{% set class = class ~ " md-nav__item--section" %}
|
||||||
|
|
||||||
|
<!-- Determine whether to prune inactive item -->
|
||||||
|
{% elif not nav_item.active and "navigation.prune" in features %}
|
||||||
|
{% set class = class ~ " md-nav__item--pruned" %}
|
||||||
|
{% set prune = true %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Render item with nested items -->
|
<!-- Nested navigation item -->
|
||||||
<li class="{{ class }} md-nav__item--nested">
|
<li class="{{ class }} md-nav__item--nested">
|
||||||
{% set expanded = "navigation.expand" in features %}
|
{% if not prune %}
|
||||||
{% set active = nav_item.active or expanded %}
|
{% set expanded = "navigation.expand" in features %}
|
||||||
|
{% set active = nav_item.active or expanded %}
|
||||||
|
|
||||||
<!-- Determine checked and indeterminate state -->
|
<!-- Determine checked and indeterminate state -->
|
||||||
{% set checked = "checked" if nav_item.active %}
|
{% set checked = "checked" if nav_item.active %}
|
||||||
{% if expanded and not checked %}
|
{% if expanded and not checked %}
|
||||||
{% set indeterminate = "md-toggle--indeterminate" %}
|
{% set indeterminate = "md-toggle--indeterminate" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Active checkbox expands items contained within nested section -->
|
<!-- Active checkbox expands items contained within nested section -->
|
||||||
<input
|
<input
|
||||||
class="md-nav__toggle md-toggle {{ indeterminate }}"
|
class="md-nav__toggle md-toggle {{ indeterminate }}"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="{{ path }}"
|
id="{{ path }}"
|
||||||
{{ checked }}
|
{{ checked }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Determine all nested items that are index pages -->
|
<!-- Toggle to expand nested items -->
|
||||||
{% set indexes = [] %}
|
{% if not indexes %}
|
||||||
{% if "navigation.indexes" in features %}
|
<label
|
||||||
{% for nav_item in nav_item.children %}
|
class="md-nav__link"
|
||||||
{% if nav_item.is_index and not index is defined %}
|
for="{{ path }}"
|
||||||
{% set _ = indexes.append(nav_item) %}
|
id="{{ path }}_label"
|
||||||
{% endif %}
|
tabindex="0"
|
||||||
{% endfor %}
|
>
|
||||||
{% endif %}
|
{{ render_content(nav_item) }}
|
||||||
|
<span class="md-nav__icon md-icon"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<!-- Render toggle to expand nested items -->
|
<!-- Toggle to expand nested items with link to index page -->
|
||||||
{% if not indexes %}
|
{% else %}
|
||||||
<label
|
{% set index = indexes | first %}
|
||||||
class="md-nav__link"
|
{% set class = "md-nav__link--active" if index == page %}
|
||||||
for="{{ path }}"
|
<div class="md-nav__link md-nav__container">
|
||||||
id="{{ path }}_label"
|
<a
|
||||||
tabindex="0"
|
href="{{ index.url | url }}"
|
||||||
>
|
class="md-nav__link {{ class }}"
|
||||||
{{ nav_item.title }}
|
>
|
||||||
<span class="md-nav__icon md-icon"></span>
|
{{ render_content(index, nav_item) }}
|
||||||
</label>
|
</a>
|
||||||
|
|
||||||
<!-- Render link to index page + toggle -->
|
<!-- Only render toggle if there's at least one more page -->
|
||||||
{% else %}
|
{% if nav_item.children | length > 1 %}
|
||||||
{% set index = indexes | first %}
|
<label class="md-nav__link {{ class }}" for="{{ path }}">
|
||||||
{% set class = "md-nav__link--active" if index == page %}
|
<span class="md-nav__icon md-icon"></span>
|
||||||
<div class="md-nav__link md-nav__link--index {{ class }}">
|
</label>
|
||||||
<a href="{{ index.url | url }}">{{ nav_item.title }}</a>
|
|
||||||
|
|
||||||
<!-- Only render toggle if there's at least one more page -->
|
|
||||||
{% if nav_item.children | length > 1 %}
|
|
||||||
<label for="{{ path }}">
|
|
||||||
<span class="md-nav__icon md-icon"></span>
|
|
||||||
</label>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Render nested navigation -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Render nested item list -->
|
|
||||||
{% for nav_item in nav_item.children %}
|
|
||||||
{% if not indexes or nav_item != indexes | first %}
|
|
||||||
{{ render(nav_item, path ~ "_" ~ loop.index, level + 1) }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</ul>
|
{% endif %}
|
||||||
</nav>
|
|
||||||
|
<!-- Nested navigation -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Nested navigation item -->
|
||||||
|
{% 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>
|
||||||
|
|
||||||
|
<!-- Pruned navigation item -->
|
||||||
|
{% else %}
|
||||||
|
{{ render_pruned(nav_item) }}
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Currently active page -->
|
<!-- Currently active page -->
|
||||||
@ -124,7 +198,7 @@
|
|||||||
<li class="{{ class }}">
|
<li class="{{ class }}">
|
||||||
{% set toc = page.toc %}
|
{% set toc = page.toc %}
|
||||||
|
|
||||||
<!-- Active checkbox expands items contained within nested section -->
|
<!-- State toggle -->
|
||||||
<input
|
<input
|
||||||
class="md-nav__toggle md-toggle"
|
class="md-nav__toggle md-toggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -137,10 +211,10 @@
|
|||||||
{% set toc = first.children %}
|
{% set toc = first.children %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Render table of contents, if not empty -->
|
<!-- Navigation link to table of contents -->
|
||||||
{% if toc %}
|
{% if toc %}
|
||||||
<label class="md-nav__link md-nav__link--active" for="__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>
|
<span class="md-nav__icon md-icon"></span>
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -148,24 +222,21 @@
|
|||||||
href="{{ nav_item.url | url }}"
|
href="{{ nav_item.url | url }}"
|
||||||
class="md-nav__link md-nav__link--active"
|
class="md-nav__link md-nav__link--active"
|
||||||
>
|
>
|
||||||
{{ nav_item.title }}
|
{{ render_content(nav_item) }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Show table of contents -->
|
<!-- Table of contents -->
|
||||||
{% if toc %}
|
{% if toc %}
|
||||||
{% include "partials/toc.html" %}
|
{% include "partials/toc.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Main navigation item -->
|
<!-- Navigation item -->
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="{{ class }}">
|
<li class="{{ class }}">
|
||||||
<a href="{{ nav_item.url | url }}" class="md-nav__link">
|
<a href="{{ nav_item.url | url }}" class="md-nav__link">
|
||||||
{{ nav_item.title }}
|
{{ render_content(nav_item) }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<!-- Render current and nested navigation items -->
|
|
||||||
{{ render(nav_item, path, level) }}
|
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
IN THE SOFTWARE.
|
IN THE SOFTWARE.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Determine class according to configuration -->
|
{% import "partials/nav-item.html" as item with context %}
|
||||||
|
|
||||||
|
<!-- Determine base classes -->
|
||||||
{% set class = "md-nav md-nav--primary" %}
|
{% set class = "md-nav md-nav--primary" %}
|
||||||
{% if "navigation.tabs" in features %}
|
{% if "navigation.tabs" in features %}
|
||||||
{% set class = class ~ " md-nav--lifted" %}
|
{% set class = class ~ " md-nav--lifted" %}
|
||||||
@ -29,7 +31,7 @@
|
|||||||
{% set class = class ~ " md-nav--integrated" %}
|
{% set class = class ~ " md-nav--integrated" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Main navigation -->
|
<!-- Navigation -->
|
||||||
<nav
|
<nav
|
||||||
class="{{ class }}"
|
class="{{ class }}"
|
||||||
aria-label="{{ lang.t('nav') }}"
|
aria-label="{{ lang.t('nav') }}"
|
||||||
@ -57,12 +59,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Render item list -->
|
<!-- Navigation list -->
|
||||||
<ul class="md-nav__list" data-md-scrollfix>
|
<ul class="md-nav__list" data-md-scrollfix>
|
||||||
{% for nav_item in nav %}
|
{% for nav_item in nav %}
|
||||||
{% set path = "__nav_" ~ loop.index %}
|
{% set path = "__nav_" ~ loop.index %}
|
||||||
{% set level = 1 %}
|
{{ item.render(nav_item, path, 1) }}
|
||||||
{% include "partials/nav-item.html" %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
42
src/partials/pagination.html
Normal file
42
src/partials/pagination.html
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Pagination icons -->
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<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>
|
99
src/partials/post.html
Normal file
99
src/partials/post.html
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Post excerpt -->
|
||||||
|
<article class="md-post md-post--excerpt">
|
||||||
|
<header class="md-post__header">
|
||||||
|
|
||||||
|
<!-- Post authors -->
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<!-- Post metadata -->
|
||||||
|
<div class="md-post__meta md-meta">
|
||||||
|
<ul class="md-meta__list">
|
||||||
|
|
||||||
|
<!-- Post date -->
|
||||||
|
<li class="md-meta__item">
|
||||||
|
<time datetime="{{ post.meta.date }}">
|
||||||
|
{{- post.meta.date_format -}}
|
||||||
|
</time>
|
||||||
|
{#- Collapse whitespace -#}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Post categories -->
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<!-- Post readtime -->
|
||||||
|
{% if post.meta.readtime %}
|
||||||
|
{% set time = post.meta.readtime %}
|
||||||
|
<li class="md-meta__item">
|
||||||
|
{% if time == 1 %}
|
||||||
|
{{ lang.t("readtime.one") }}
|
||||||
|
{% else %}
|
||||||
|
{{ lang.t("readtime.other") | replace("#", time) }}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Draft marker -->
|
||||||
|
{% if post.meta.draft %}
|
||||||
|
<span class="md-draft">
|
||||||
|
{{ lang.t("blog.draft") }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Post content -->
|
||||||
|
<div class="md-post__content md-typeset">
|
||||||
|
{{ post.content }}
|
||||||
|
|
||||||
|
<!-- Continue reading link -->
|
||||||
|
<nav class="md-post__action">
|
||||||
|
<a href="{{ post.url | url }}">
|
||||||
|
{{ lang.t("blog.continue") }}
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</article>
|
@ -20,37 +20,52 @@
|
|||||||
IN THE SOFTWARE.
|
IN THE SOFTWARE.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Determine class according to state -->
|
<!-- Render navigation link content -->
|
||||||
{% if not class %}
|
{% macro render_content(nav_item, ref = nav_item) %}
|
||||||
|
|
||||||
|
<!-- Navigation link icon -->
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<!-- Navigation link title -->
|
||||||
|
{{ ref.title }}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
<!-- Render navigation item -->
|
||||||
|
{% macro render(nav_item, ref = nav_item) %}
|
||||||
|
|
||||||
|
<!-- Determine class according to state -->
|
||||||
{% set class = "md-tabs__link" %}
|
{% set class = "md-tabs__link" %}
|
||||||
{% if nav_item.active %}
|
{% if ref.active %}
|
||||||
{% set class = class ~ " md-tabs__link--active" %}
|
{% set class = class ~ " md-tabs__link--active" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Main navigation item with nested items -->
|
<!-- Navigation item with nested items -->
|
||||||
{% if nav_item.children %}
|
|
||||||
{% set title = title | d(nav_item.title) %}
|
|
||||||
{% set nav_item = nav_item.children | first %}
|
|
||||||
|
|
||||||
<!-- Recurse, if the first item has further nested items -->
|
|
||||||
{% if nav_item.children %}
|
{% if nav_item.children %}
|
||||||
{% include "partials/tabs-item.html" %}
|
{% set first = nav_item.children | first %}
|
||||||
|
|
||||||
<!-- Render item -->
|
<!-- Recurse, if the first item has further nested items -->
|
||||||
|
{% if first.children %}
|
||||||
|
{{ render(first, ref) }}
|
||||||
|
|
||||||
|
<!-- Nested navigation item -->
|
||||||
|
{% else %}
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="{{ first.url | url }}" class="{{ class }}">
|
||||||
|
{{ render_content(first, ref) }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Navigation item -->
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="md-tabs__item">
|
<li class="md-tabs__item">
|
||||||
<a href="{{ nav_item.url | url }}" class="{{ class }}">
|
<a href="{{ nav_item.url | url }}" class="{{ class }}">
|
||||||
{{ title }}
|
{{ render_content(nav_item) }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
<!-- Main navigation item -->
|
|
||||||
{% else %}
|
|
||||||
<li class="md-tabs__item">
|
|
||||||
<a href="{{ nav_item.url | url }}" class="{{ class }}">
|
|
||||||
{{ nav_item.title }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
@ -20,8 +20,7 @@
|
|||||||
IN THE SOFTWARE.
|
IN THE SOFTWARE.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Hack: unset variable, as we're using it recursively in tabs-item.html -->
|
{% import "partials/tabs-item.html" as item with context %}
|
||||||
{% set class = "" %}
|
|
||||||
|
|
||||||
<!-- Navigation tabs -->
|
<!-- Navigation tabs -->
|
||||||
<nav
|
<nav
|
||||||
@ -32,7 +31,7 @@
|
|||||||
<div class="md-grid">
|
<div class="md-grid">
|
||||||
<ul class="md-tabs__list">
|
<ul class="md-tabs__list">
|
||||||
{% for nav_item in nav %}
|
{% for nav_item in nav %}
|
||||||
{% include "partials/tabs-item.html" %}
|
{{ item.render(nav_item) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
0
src/plugins/blog/__init__.py
Normal file
0
src/plugins/blog/__init__.py
Normal file
82
src/plugins/blog/config.py
Normal file
82
src/plugins/blog/config.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Blog plugin configuration scheme
|
||||||
|
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_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_template = Type(str, default = "~2~")
|
||||||
|
|
||||||
|
# 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)
|
887
src/plugins/blog/plugin.py
Normal file
887
src/plugins/blog/plugin.py
Normal file
@ -0,0 +1,887 @@
|
|||||||
|
# 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 paginate
|
||||||
|
import posixpath
|
||||||
|
import re
|
||||||
|
import readtime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from babel.dates import format_date
|
||||||
|
from copy import copy
|
||||||
|
from datetime import date, datetime, time
|
||||||
|
from hashlib import sha1
|
||||||
|
from lxml.html import fragment_fromstring, tostring
|
||||||
|
from mkdocs import utils
|
||||||
|
from mkdocs.utils.meta import get_data
|
||||||
|
from mkdocs.commands.build import _populate_page
|
||||||
|
from mkdocs.contrib.search import SearchIndex
|
||||||
|
from mkdocs.plugins import BasePlugin
|
||||||
|
from mkdocs.structure.files import File, Files
|
||||||
|
from mkdocs.structure.nav import Link, Section
|
||||||
|
from mkdocs.structure.pages import Page
|
||||||
|
from tempfile import gettempdir
|
||||||
|
from yaml import SafeLoader, load
|
||||||
|
|
||||||
|
from material.plugins.blog.config import BlogConfig
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Blog plugin
|
||||||
|
class BlogPlugin(BasePlugin[BlogConfig]):
|
||||||
|
supports_multiple_instances = True
|
||||||
|
|
||||||
|
# Determine whether we're running under dirty reload
|
||||||
|
def on_startup(self, *, command, dirty):
|
||||||
|
self.is_serve = (command == "serve")
|
||||||
|
self.is_dirtyreload = False
|
||||||
|
self.is_dirty = dirty
|
||||||
|
|
||||||
|
# Initialize plugin
|
||||||
|
def on_config(self, config):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Resolve source directory for posts and generated files
|
||||||
|
self.post_dir = self._resolve("posts")
|
||||||
|
self.temp_dir = gettempdir()
|
||||||
|
|
||||||
|
# Initialize posts
|
||||||
|
self.post_map = dict()
|
||||||
|
self.post_meta_map = dict()
|
||||||
|
self.post_pages = []
|
||||||
|
self.post_pager_pages = []
|
||||||
|
|
||||||
|
# Initialize archive
|
||||||
|
if self.config.archive:
|
||||||
|
self.archive_map = dict()
|
||||||
|
self.archive_post_map = dict()
|
||||||
|
|
||||||
|
# Initialize categories
|
||||||
|
if self.config.categories:
|
||||||
|
self.category_map = dict()
|
||||||
|
self.category_name_map = dict()
|
||||||
|
self.category_post_map = dict()
|
||||||
|
|
||||||
|
# Initialize authors
|
||||||
|
if self.config.authors:
|
||||||
|
self.authors_map = dict()
|
||||||
|
|
||||||
|
# Resolve authors file
|
||||||
|
path = os.path.normpath(os.path.join(
|
||||||
|
config.docs_dir,
|
||||||
|
self.config.authors_file.format(
|
||||||
|
blog = self.config.blog_dir
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Load authors map, if it exists
|
||||||
|
if os.path.isfile(path):
|
||||||
|
with open(path, encoding = "utf-8") as f:
|
||||||
|
self.authors_map = load(f, SafeLoader) or {}
|
||||||
|
|
||||||
|
# Ensure that format strings have no trailing slashes
|
||||||
|
for option in [
|
||||||
|
"post_url_format",
|
||||||
|
"archive_url_format",
|
||||||
|
"categories_url_format",
|
||||||
|
"pagination_url_format"
|
||||||
|
]:
|
||||||
|
if self.config[option].endswith("/"):
|
||||||
|
log.error(f"Option '{option}' must not contain trailing slash.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Inherit global table of contents setting
|
||||||
|
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
|
||||||
|
|
||||||
|
# If pagination should not be used, set to large value
|
||||||
|
if not self.config.pagination:
|
||||||
|
self.config.pagination_per_page = 1e7
|
||||||
|
|
||||||
|
# By default, drafts are rendered when the documentation is served,
|
||||||
|
# but not when it is built. This should nicely align with the expected
|
||||||
|
# user experience when authoring documentation.
|
||||||
|
if self.is_serve and self.config.draft_on_serve:
|
||||||
|
self.config.draft = True
|
||||||
|
|
||||||
|
# Adjust paths to assets in the posts directory and preprocess posts
|
||||||
|
def on_files(self, files, *, config):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Adjust destination paths for assets
|
||||||
|
path = self._resolve("assets")
|
||||||
|
for file in files.media_files():
|
||||||
|
if self.post_dir not in file.src_uri:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Compute destination URL
|
||||||
|
file.url = file.url.replace(self.post_dir, path)
|
||||||
|
|
||||||
|
# Compute destination file system path
|
||||||
|
file.dest_uri = file.dest_uri.replace(self.post_dir, path)
|
||||||
|
file.abs_dest_path = os.path.join(config.site_dir, file.dest_path)
|
||||||
|
|
||||||
|
# Hack: as post URLs are dynamically computed and can be configured by
|
||||||
|
# the author, we need to compute them before we process the contents of
|
||||||
|
# any other page or post. If we wouldn't do that, URLs would be invalid
|
||||||
|
# and we would need to patch them afterwards. The only way to do this
|
||||||
|
# correctly is to first extract the metadata of all posts. Additionally,
|
||||||
|
# while we're at it, generate all archive and category pages as we have
|
||||||
|
# the post metadata on our hands. This ensures that we can safely link
|
||||||
|
# from anywhere to all pages that are generated as part of the blog.
|
||||||
|
for file in files.documentation_pages():
|
||||||
|
if self.post_dir not in file.src_uri:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Read and preprocess post
|
||||||
|
with open(file.abs_src_path, encoding = "utf-8") as f:
|
||||||
|
markdown, meta = get_data(f.read())
|
||||||
|
|
||||||
|
# Ensure post has a date set
|
||||||
|
if not meta.get("date"):
|
||||||
|
log.error(f"Blog post '{file.src_uri}' has no date set.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Compute slug from metadata, content or file name
|
||||||
|
headline = utils.get_markdown_title(markdown)
|
||||||
|
slug = meta.get("title", headline or file.name)
|
||||||
|
|
||||||
|
# Front matter can be defind in YAML, guarded by two lines with
|
||||||
|
# `---` markers, or MultiMarkdown, separated by an empty line.
|
||||||
|
# If the author chooses to use MultiMarkdown syntax, date is
|
||||||
|
# returned as a string, which is different from YAML behavior,
|
||||||
|
# which returns a date. Thus, we must check for its type, and
|
||||||
|
# parse the date for normalization purposes.
|
||||||
|
if isinstance(meta["date"], str):
|
||||||
|
meta["date"] = date.fromisoformat(meta["date"])
|
||||||
|
|
||||||
|
# Normalize date to datetime for proper sorting
|
||||||
|
if not isinstance(meta["date"], datetime):
|
||||||
|
meta["date"] = datetime.combine(meta["date"], time())
|
||||||
|
|
||||||
|
# Compute category slugs
|
||||||
|
categories = []
|
||||||
|
for name in meta.get("categories", []):
|
||||||
|
categories.append(self.config.categories_slugify(
|
||||||
|
name, self.config.categories_slugify_separator
|
||||||
|
))
|
||||||
|
|
||||||
|
# Check if maximum number of categories is reached
|
||||||
|
max_categories = self.config.post_url_max_categories
|
||||||
|
if len(categories) == max_categories:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Compute path from format string
|
||||||
|
date_format = self.config.post_url_date_format
|
||||||
|
path = self.config.post_url_format.format(
|
||||||
|
categories = "/".join(categories),
|
||||||
|
date = self._format_date(meta["date"], date_format, config),
|
||||||
|
file = file.name,
|
||||||
|
slug = meta.get("slug", self.config.post_slugify(
|
||||||
|
slug, self.config.post_slugify_separator
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize path, as it may begin with a slash
|
||||||
|
path = posixpath.normpath("/".join([".", path]))
|
||||||
|
|
||||||
|
# Compute destination URL according to settings
|
||||||
|
file.url = self._resolve(path)
|
||||||
|
if not config.use_directory_urls:
|
||||||
|
file.url += ".html"
|
||||||
|
else:
|
||||||
|
file.url += "/"
|
||||||
|
|
||||||
|
# Compute destination file system path
|
||||||
|
file.dest_uri = re.sub(r"(?<=\/)$", "index.html", file.url)
|
||||||
|
file.abs_dest_path = os.path.join(
|
||||||
|
config.site_dir, file.dest_path
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add post metadata
|
||||||
|
self.post_meta_map[file.src_uri] = meta
|
||||||
|
|
||||||
|
# Sort post metadata by date (descending)
|
||||||
|
self.post_meta_map = dict(sorted(
|
||||||
|
self.post_meta_map.items(),
|
||||||
|
key = lambda item: item[1]["date"], reverse = True
|
||||||
|
))
|
||||||
|
|
||||||
|
# Find and extract the section hosting the blog
|
||||||
|
path = self._resolve("index.md")
|
||||||
|
root = _host(config.nav, path)
|
||||||
|
|
||||||
|
# Ensure blog root exists
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if not file:
|
||||||
|
log.error(f"Blog root '{path}' does not exist.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Ensure blog root is part of navigation
|
||||||
|
if not root:
|
||||||
|
log.error(f"Blog root '{path}' not in navigation.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Generate and register files for archive
|
||||||
|
if self.config.archive:
|
||||||
|
name = self._translate(config, self.config.archive_name)
|
||||||
|
data = self._generate_files_for_archive(config, files)
|
||||||
|
if data:
|
||||||
|
root.append({ name: data })
|
||||||
|
|
||||||
|
# Generate and register files for categories
|
||||||
|
if self.config.categories:
|
||||||
|
name = self._translate(config, self.config.categories_name)
|
||||||
|
data = self._generate_files_for_categories(config, files)
|
||||||
|
if data:
|
||||||
|
root.append({ name: data })
|
||||||
|
|
||||||
|
# Hack: add posts temporarily, so MkDocs doesn't complain
|
||||||
|
name = sha1(path.encode("utf-8")).hexdigest()
|
||||||
|
root.append({
|
||||||
|
f"__posts_${name}": list(self.post_meta_map.keys())
|
||||||
|
})
|
||||||
|
|
||||||
|
# Cleanup navigation before proceeding
|
||||||
|
def on_nav(self, nav, *, config, files):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find and resolve index for cleanup
|
||||||
|
path = self._resolve("index.md")
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
|
||||||
|
# Determine blog root section
|
||||||
|
self.main = file.page
|
||||||
|
if self.main.parent:
|
||||||
|
root = self.main.parent.children
|
||||||
|
else:
|
||||||
|
root = nav.items
|
||||||
|
|
||||||
|
# Hack: remove temporarily added posts from the navigation
|
||||||
|
name = sha1(path.encode("utf-8")).hexdigest()
|
||||||
|
for item in root:
|
||||||
|
if not item.is_section or item.title != f"__posts_${name}":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Detach previous and next links of posts
|
||||||
|
if item.children:
|
||||||
|
head = item.children[+0]
|
||||||
|
tail = item.children[-1]
|
||||||
|
|
||||||
|
# Link page prior to posts to page after posts
|
||||||
|
if head.previous_page:
|
||||||
|
head.previous_page.next_page = tail.next_page
|
||||||
|
|
||||||
|
# Link page after posts to page prior to posts
|
||||||
|
if tail.next_page:
|
||||||
|
tail.next_page.previous_page = head.previous_page
|
||||||
|
|
||||||
|
# Contain previous and next links inside posts
|
||||||
|
head.previous_page = None
|
||||||
|
tail.next_page = None
|
||||||
|
|
||||||
|
# Set blog as parent page
|
||||||
|
for page in item.children:
|
||||||
|
page.parent = self.main
|
||||||
|
next = page.next_page
|
||||||
|
|
||||||
|
# Switch previous and next links
|
||||||
|
page.next_page = page.previous_page
|
||||||
|
page.previous_page = next
|
||||||
|
|
||||||
|
# Remove posts from navigation
|
||||||
|
root.remove(item)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Prepare post for rendering
|
||||||
|
def on_page_markdown(self, markdown, *, page, config, files):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only process posts
|
||||||
|
if self.post_dir not in page.file.src_uri:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip processing of drafts
|
||||||
|
if self._is_draft(page.file.src_uri):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure template is set or use default
|
||||||
|
if "template" not in page.meta:
|
||||||
|
page.meta["template"] = "blog-post.html"
|
||||||
|
|
||||||
|
# Use previously normalized date
|
||||||
|
page.meta["date"] = self.post_meta_map[page.file.src_uri]["date"]
|
||||||
|
|
||||||
|
# Ensure navigation is hidden
|
||||||
|
page.meta["hide"] = page.meta.get("hide", [])
|
||||||
|
if "navigation" not in page.meta["hide"]:
|
||||||
|
page.meta["hide"].append("navigation")
|
||||||
|
|
||||||
|
# Format date for rendering
|
||||||
|
date_format = self.config.post_date_format
|
||||||
|
page.meta["date_format"] = self._format_date(
|
||||||
|
page.meta["date"], date_format, config
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compute readtime if desired and not explicitly set
|
||||||
|
if self.config.post_readtime:
|
||||||
|
|
||||||
|
# There's a bug in the readtime library, which causes it to fail
|
||||||
|
# when the input string contains emojis (reported in #5555)
|
||||||
|
encoded = markdown.encode("unicode_escape")
|
||||||
|
if "readtime" not in page.meta:
|
||||||
|
rate = self.config.post_readtime_words_per_minute
|
||||||
|
read = readtime.of_markdown(encoded, rate)
|
||||||
|
page.meta["readtime"] = read.minutes
|
||||||
|
|
||||||
|
# Compute post categories
|
||||||
|
page.categories = []
|
||||||
|
if self.config.categories:
|
||||||
|
for name in page.meta.get("categories", []):
|
||||||
|
file = files.get_file_from_path(self.category_name_map[name])
|
||||||
|
page.categories.append(file.page)
|
||||||
|
|
||||||
|
# Compute post authors
|
||||||
|
page.authors = []
|
||||||
|
if self.config.authors:
|
||||||
|
for name in page.meta.get("authors", []):
|
||||||
|
if name not in self.authors_map:
|
||||||
|
log.error(
|
||||||
|
f"Blog post '{page.file.src_uri}' author '{name}' "
|
||||||
|
f"unknown, not listed in .authors.yml"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Add author to page
|
||||||
|
page.authors.append(self.authors_map[name])
|
||||||
|
|
||||||
|
# Fix stale link if previous post is a draft
|
||||||
|
prev = page.previous_page
|
||||||
|
while prev and self._is_draft(prev.file.src_uri):
|
||||||
|
page.previous_page = prev.previous_page
|
||||||
|
prev = prev.previous_page
|
||||||
|
|
||||||
|
# Fix stale link if next post is a draft
|
||||||
|
next = page.next_page
|
||||||
|
while next and self._is_draft(next.file.src_uri):
|
||||||
|
page.next_page = next.next_page
|
||||||
|
next = next.next_page
|
||||||
|
|
||||||
|
# Filter posts and generate excerpts for generated pages
|
||||||
|
def on_env(self, env, *, config, files):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip post excerpts on dirty reload to save time
|
||||||
|
if self.is_dirtyreload:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Copy configuration and enable 'toc' extension
|
||||||
|
config = copy(config)
|
||||||
|
config.mdx_configs["toc"] = copy(config.mdx_configs.get("toc", {}))
|
||||||
|
|
||||||
|
# Ensure that post titles are links
|
||||||
|
config.mdx_configs["toc"]["anchorlink"] = True
|
||||||
|
config.mdx_configs["toc"]["permalink"] = False
|
||||||
|
|
||||||
|
# Filter posts that should not be published
|
||||||
|
for file in files.documentation_pages():
|
||||||
|
if self.post_dir in file.src_uri:
|
||||||
|
if self._is_draft(file.src_uri):
|
||||||
|
files.remove(file)
|
||||||
|
|
||||||
|
# Ensure template is set
|
||||||
|
if "template" not in self.main.meta:
|
||||||
|
self.main.meta["template"] = "blog.html"
|
||||||
|
|
||||||
|
# Populate archive
|
||||||
|
if self.config.archive:
|
||||||
|
for path in self.archive_map:
|
||||||
|
self.archive_post_map[path] = []
|
||||||
|
|
||||||
|
# Generate post excerpts for archive
|
||||||
|
base = files.get_file_from_path(path)
|
||||||
|
for file in self.archive_map[path]:
|
||||||
|
self.archive_post_map[path].append(
|
||||||
|
self._generate_excerpt(file, base, config, files)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure template is set
|
||||||
|
page = base.page
|
||||||
|
if "template" not in page.meta:
|
||||||
|
page.meta["template"] = "blog-archive.html"
|
||||||
|
|
||||||
|
# Populate categories
|
||||||
|
if self.config.categories:
|
||||||
|
for path in self.category_map:
|
||||||
|
self.category_post_map[path] = []
|
||||||
|
|
||||||
|
# Generate post excerpts for categories
|
||||||
|
base = files.get_file_from_path(path)
|
||||||
|
for file in self.category_map[path]:
|
||||||
|
self.category_post_map[path].append(
|
||||||
|
self._generate_excerpt(file, base, config, files)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure template is set
|
||||||
|
page = base.page
|
||||||
|
if "template" not in page.meta:
|
||||||
|
page.meta["template"] = "blog-category.html"
|
||||||
|
|
||||||
|
# Resolve path of initial index
|
||||||
|
curr = self._resolve("index.md")
|
||||||
|
base = self.main.file
|
||||||
|
|
||||||
|
# Initialize index
|
||||||
|
self.post_map[curr] = []
|
||||||
|
self.post_pager_pages.append(self.main)
|
||||||
|
|
||||||
|
# Generate indexes by paginating through posts
|
||||||
|
for path in self.post_meta_map.keys():
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if not self._is_draft(path):
|
||||||
|
self.post_pages.append(file.page)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Generate new index when the current is full
|
||||||
|
per_page = self.config.pagination_per_page
|
||||||
|
if len(self.post_map[curr]) == per_page:
|
||||||
|
offset = 1 + len(self.post_map)
|
||||||
|
|
||||||
|
# Resolve path of new index
|
||||||
|
curr = self.config.pagination_url_format.format(page = offset)
|
||||||
|
curr = self._resolve(curr + ".md")
|
||||||
|
|
||||||
|
# Generate file
|
||||||
|
self._generate_file(curr, f"# {self.main.title}")
|
||||||
|
|
||||||
|
# Register file and page
|
||||||
|
base = self._register_file(curr, config, files)
|
||||||
|
page = self._register_page(base, config, files)
|
||||||
|
|
||||||
|
# Inherit page metadata, title and position
|
||||||
|
page.meta = self.main.meta
|
||||||
|
page.title = self.main.title
|
||||||
|
page.parent = self.main
|
||||||
|
page.previous_page = self.main.previous_page
|
||||||
|
page.next_page = self.main.next_page
|
||||||
|
|
||||||
|
# Initialize next index
|
||||||
|
self.post_map[curr] = []
|
||||||
|
self.post_pager_pages.append(page)
|
||||||
|
|
||||||
|
# Assign post excerpt to current index
|
||||||
|
self.post_map[curr].append(
|
||||||
|
self._generate_excerpt(file, base, config, files)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Populate generated pages
|
||||||
|
def on_page_context(self, context, *, page, config, nav):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Provide post excerpts for index
|
||||||
|
path = page.file.src_uri
|
||||||
|
if path in self.post_map:
|
||||||
|
context["posts"] = self.post_map[path]
|
||||||
|
if self.config.blog_toc:
|
||||||
|
self._populate_toc(page, context["posts"])
|
||||||
|
|
||||||
|
# Create pagination
|
||||||
|
pagination = paginate.Page(
|
||||||
|
self.post_pages,
|
||||||
|
page = list(self.post_map.keys()).index(path) + 1,
|
||||||
|
items_per_page = self.config.pagination_per_page,
|
||||||
|
url_maker = lambda n: utils.get_relative_url(
|
||||||
|
self.post_pager_pages[n - 1].url,
|
||||||
|
page.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create pagination pager
|
||||||
|
context["pagination"] = lambda args: pagination.pager(
|
||||||
|
format = self.config.pagination_template,
|
||||||
|
show_if_single_page = False,
|
||||||
|
**args
|
||||||
|
)
|
||||||
|
|
||||||
|
# Provide post excerpts for archive
|
||||||
|
if self.config.archive:
|
||||||
|
if path in self.archive_post_map:
|
||||||
|
context["posts"] = self.archive_post_map[path]
|
||||||
|
if self.config.archive_toc:
|
||||||
|
self._populate_toc(page, context["posts"])
|
||||||
|
|
||||||
|
# Provide post excerpts for categories
|
||||||
|
if self.config.categories:
|
||||||
|
if path in self.category_post_map:
|
||||||
|
context["posts"] = self.category_post_map[path]
|
||||||
|
if self.config.categories_toc:
|
||||||
|
self._populate_toc(page, context["posts"])
|
||||||
|
|
||||||
|
# Determine whether we're running under dirty reload
|
||||||
|
def on_serve(self, server, *, config, builder):
|
||||||
|
self.is_dirtyreload = self.is_dirty
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Generate and register files for archive
|
||||||
|
def _generate_files_for_archive(self, config, files):
|
||||||
|
for path, meta in self.post_meta_map.items():
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if self._is_draft(path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Compute name from format string
|
||||||
|
date_format = self.config.archive_date_format
|
||||||
|
name = self._format_date(meta["date"], date_format, config)
|
||||||
|
|
||||||
|
# Compute path from format string
|
||||||
|
date_format = self.config.archive_url_date_format
|
||||||
|
path = self.config.archive_url_format.format(
|
||||||
|
date = self._format_date(meta["date"], date_format, config)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create file for archive if it doesn't exist
|
||||||
|
path = self._resolve(path + ".md")
|
||||||
|
if path not in self.archive_map:
|
||||||
|
self.archive_map[path] = []
|
||||||
|
|
||||||
|
# Generate and register file for archive
|
||||||
|
self._generate_file(path, f"# {name}")
|
||||||
|
self._register_file(path, config, files)
|
||||||
|
|
||||||
|
# Assign current post to archive
|
||||||
|
self.archive_map[path].append(file)
|
||||||
|
|
||||||
|
# Return generated archive files
|
||||||
|
return list(self.archive_map.keys())
|
||||||
|
|
||||||
|
# Generate and register files for categories
|
||||||
|
def _generate_files_for_categories(self, config, files):
|
||||||
|
allowed = set(self.config.categories_allowed)
|
||||||
|
for path, meta in self.post_meta_map.items():
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if self._is_draft(path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ensure category is in (non-empty) allow list
|
||||||
|
categories = set(meta.get("categories", []))
|
||||||
|
if allowed:
|
||||||
|
for name in categories - allowed:
|
||||||
|
log.error(
|
||||||
|
f"Blog post '{file.src_uri}' uses a category "
|
||||||
|
f"which is not in allow list: {name}"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Traverse all categories of the post
|
||||||
|
for name in categories:
|
||||||
|
path = self.config.categories_url_format.format(
|
||||||
|
slug = self.config.categories_slugify(
|
||||||
|
name, self.config.categories_slugify_separator
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create file for category if it doesn't exist
|
||||||
|
path = self._resolve(path + ".md")
|
||||||
|
if path not in self.category_map:
|
||||||
|
self.category_map[path] = []
|
||||||
|
|
||||||
|
# Generate and register file for category
|
||||||
|
self._generate_file(path, f"# {name}")
|
||||||
|
self._register_file(path, config, files)
|
||||||
|
|
||||||
|
# Link category path to name
|
||||||
|
self.category_name_map[name] = path
|
||||||
|
|
||||||
|
# Assign current post to category
|
||||||
|
self.category_map[path].append(file)
|
||||||
|
|
||||||
|
# Sort categories alphabetically (ascending)
|
||||||
|
self.category_map = dict(sorted(self.category_map.items()))
|
||||||
|
|
||||||
|
# Return generated category files
|
||||||
|
return list(self.category_map.keys())
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Check if a post is a draft
|
||||||
|
def _is_draft(self, path):
|
||||||
|
meta = self.post_meta_map[path]
|
||||||
|
if not self.config.draft:
|
||||||
|
|
||||||
|
# Check if post date is in the future
|
||||||
|
future = False
|
||||||
|
if self.config.draft_if_future_date:
|
||||||
|
future = meta["date"] > datetime.now()
|
||||||
|
|
||||||
|
# Check if post is marked as draft
|
||||||
|
return meta.get("draft", future)
|
||||||
|
|
||||||
|
# Post is not a draft
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Generate a post excerpt relative to base
|
||||||
|
def _generate_excerpt(self, file, base, config, files):
|
||||||
|
page = file.page
|
||||||
|
|
||||||
|
# Generate temporary file and page for post excerpt
|
||||||
|
temp = self._register_file(file.src_uri, config)
|
||||||
|
excerpt = Page(page.title, temp, config)
|
||||||
|
|
||||||
|
# Check for separator, if post excerpt is required
|
||||||
|
separator = self.config.post_excerpt_separator
|
||||||
|
if self.config.post_excerpt == "required":
|
||||||
|
if separator not in page.markdown:
|
||||||
|
log.error(f"Blog post '{temp.src_uri}' has no excerpt.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Ensure separator at the end to strip footnotes and patch h1-h5
|
||||||
|
markdown = "\n\n".join([page.markdown, separator])
|
||||||
|
markdown = re.sub(r"(^#{1,5})", "#\\1", markdown, flags = re.MULTILINE)
|
||||||
|
|
||||||
|
# Extract content and metadata from original post
|
||||||
|
excerpt.file.url = base.url
|
||||||
|
excerpt.markdown = markdown
|
||||||
|
excerpt.meta = page.meta
|
||||||
|
|
||||||
|
# Render post and revert page URL
|
||||||
|
excerpt.render(config, files)
|
||||||
|
excerpt.file.url = page.url
|
||||||
|
|
||||||
|
# Find all anchor links
|
||||||
|
expr = re.compile(
|
||||||
|
r"<a[^>]+href=['\"]?#[^>]+>",
|
||||||
|
re.IGNORECASE | re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace callback
|
||||||
|
first = True
|
||||||
|
def replace(match):
|
||||||
|
value = match.group()
|
||||||
|
|
||||||
|
# Handle anchor link
|
||||||
|
el = fragment_fromstring(value.encode("utf-8"))
|
||||||
|
if el.tag == "a":
|
||||||
|
nonlocal first
|
||||||
|
|
||||||
|
# Fix up each anchor link of the excerpt with a link to the
|
||||||
|
# anchor of the actual post, except for the first one – that
|
||||||
|
# one needs to go to the top of the post. A better way might
|
||||||
|
# be a Markdown extension, but for now this should be fine.
|
||||||
|
url = utils.get_relative_url(excerpt.file.url, base.url)
|
||||||
|
if first:
|
||||||
|
el.set("href", url)
|
||||||
|
else:
|
||||||
|
el.set("href", url + el.get("href"))
|
||||||
|
|
||||||
|
# From now on reference anchors
|
||||||
|
first = False
|
||||||
|
|
||||||
|
# Replace link opening tag (without closing tag)
|
||||||
|
return tostring(el, encoding = "unicode")[:-4]
|
||||||
|
|
||||||
|
# Extract excerpt from post and replace anchor links
|
||||||
|
excerpt.content = expr.sub(
|
||||||
|
replace,
|
||||||
|
excerpt.content.split(separator)[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine maximum number of authors and categories
|
||||||
|
max_authors = self.config.post_excerpt_max_authors
|
||||||
|
max_categories = self.config.post_excerpt_max_categories
|
||||||
|
|
||||||
|
# Obtain computed metadata from original post
|
||||||
|
excerpt.authors = page.authors[:max_authors]
|
||||||
|
excerpt.categories = page.categories[:max_categories]
|
||||||
|
|
||||||
|
# Return post excerpt
|
||||||
|
return excerpt
|
||||||
|
|
||||||
|
# Generate a file with the given template and content
|
||||||
|
def _generate_file(self, path, content):
|
||||||
|
content = f"---\nsearch:\n exclude: true\n---\n\n{content}"
|
||||||
|
utils.write_file(
|
||||||
|
bytes(content, "utf-8"),
|
||||||
|
os.path.join(self.temp_dir, path)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register a file
|
||||||
|
def _register_file(self, path, config, files = Files([])):
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if not file:
|
||||||
|
urls = config.use_directory_urls
|
||||||
|
file = File(path, self.temp_dir, config.site_dir, urls)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# for plugins that generate files which was introduced by the
|
||||||
|
# git-revision-date-localized-plugin - see https://bit.ly/3ZUmdBx
|
||||||
|
file.generated_by = "material/blog"
|
||||||
|
|
||||||
|
# Return file
|
||||||
|
return file
|
||||||
|
|
||||||
|
# Register and populate a page
|
||||||
|
def _register_page(self, file, config, files):
|
||||||
|
page = Page(None, file, config)
|
||||||
|
_populate_page(page, config, files)
|
||||||
|
return page
|
||||||
|
|
||||||
|
# Populate table of contents of given page
|
||||||
|
def _populate_toc(self, page, posts):
|
||||||
|
toc = page.toc.items[0]
|
||||||
|
for post in posts:
|
||||||
|
toc.children.append(post.toc.items[0])
|
||||||
|
|
||||||
|
# Remove anchors below the second level
|
||||||
|
post.toc.items[0].children = []
|
||||||
|
|
||||||
|
# Translate the given placeholder value
|
||||||
|
def _translate(self, config, value):
|
||||||
|
env = config.theme.get_env()
|
||||||
|
|
||||||
|
# Load language template and return translation for placeholder
|
||||||
|
language = "partials/language.html"
|
||||||
|
template = env.get_template(language, None, { "config": config })
|
||||||
|
return template.module.t(value)
|
||||||
|
|
||||||
|
# Resolve path relative to blog root
|
||||||
|
def _resolve(self, *args):
|
||||||
|
path = posixpath.join(self.config.blog_dir, *args)
|
||||||
|
return posixpath.normpath(path)
|
||||||
|
|
||||||
|
# Format date according to locale
|
||||||
|
def _format_date(self, date, format, config):
|
||||||
|
return format_date(
|
||||||
|
date,
|
||||||
|
format = format,
|
||||||
|
locale = config.theme["language"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Helper functions
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Search the given navigation section (from the configuration) recursively to
|
||||||
|
# find the section to host all generated pages (archive, categories, etc.)
|
||||||
|
def _host(nav, path):
|
||||||
|
|
||||||
|
# Search navigation dictionary
|
||||||
|
if isinstance(nav, dict):
|
||||||
|
for _, item in nav.items():
|
||||||
|
result = _host(item, path)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Search navigation list
|
||||||
|
elif isinstance(nav, list):
|
||||||
|
if path in nav:
|
||||||
|
return nav
|
||||||
|
|
||||||
|
# Search each list item
|
||||||
|
for item in nav:
|
||||||
|
if isinstance(item, dict) and path in item.values():
|
||||||
|
if path in item.values():
|
||||||
|
return nav
|
||||||
|
else:
|
||||||
|
result = _host(item, path)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Copied and adapted from MkDocs, because we need to return existing pages and
|
||||||
|
# support anchor names as subtitles, which is pretty fucking cool.
|
||||||
|
def _data_to_navigation(nav, config, files):
|
||||||
|
|
||||||
|
# Search navigation dictionary
|
||||||
|
if isinstance(nav, dict):
|
||||||
|
return [
|
||||||
|
_data_to_navigation((key, value), config, files)
|
||||||
|
if isinstance(value, str) else
|
||||||
|
Section(
|
||||||
|
title = key,
|
||||||
|
children = _data_to_navigation(value, config, files)
|
||||||
|
)
|
||||||
|
for key, value in nav.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Search navigation list
|
||||||
|
elif isinstance(nav, list):
|
||||||
|
return [
|
||||||
|
_data_to_navigation(item, config, files)[0]
|
||||||
|
if isinstance(item, dict) and len(item) == 1 else
|
||||||
|
_data_to_navigation(item, config, files)
|
||||||
|
for item in nav
|
||||||
|
]
|
||||||
|
|
||||||
|
# Extract navigation title and path and split anchors
|
||||||
|
title, path = nav if isinstance(nav, tuple) else (None, nav)
|
||||||
|
path, _, anchor = path.partition("#")
|
||||||
|
|
||||||
|
# Try to retrieve existing file
|
||||||
|
file = files.get_file_from_path(path)
|
||||||
|
if not file:
|
||||||
|
return Link(title, path)
|
||||||
|
|
||||||
|
# Use resolved assets destination path
|
||||||
|
if not path.endswith(".md"):
|
||||||
|
return Link(title or os.path.basename(path), file.url)
|
||||||
|
|
||||||
|
# Generate temporary file as for post excerpts
|
||||||
|
else:
|
||||||
|
urls = config.use_directory_urls
|
||||||
|
link = File(path, config.docs_dir, config.site_dir, urls)
|
||||||
|
page = Page(title or file.page.title, link, config)
|
||||||
|
|
||||||
|
# Set destination file system path and URL from original file
|
||||||
|
link.dest_uri = file.dest_uri
|
||||||
|
link.abs_dest_path = file.abs_dest_path
|
||||||
|
link.url = file.url
|
||||||
|
|
||||||
|
# Retrieve name of anchor by misusing the search index
|
||||||
|
if anchor:
|
||||||
|
item = SearchIndex()._find_toc_by_id(file.page.toc, anchor)
|
||||||
|
|
||||||
|
# Set anchor name as subtitle
|
||||||
|
page.meta["subtitle"] = item.title
|
||||||
|
link.url += f"#{anchor}"
|
||||||
|
|
||||||
|
# Return navigation item
|
||||||
|
return page
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Data
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
log = logging.getLogger("mkdocs.material.blog")
|
36
src/plugins/info/config.py
Normal file
36
src/plugins/info/config.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Info plugin configuration scheme
|
||||||
|
class InfoConfig(Config):
|
||||||
|
enabled = Type(bool, default = True)
|
||||||
|
enabled_on_serve = Type(bool, default = False)
|
||||||
|
|
||||||
|
# Options for archive
|
||||||
|
archive = Type(bool, default = True)
|
||||||
|
archive_name = Type(str, default = "example")
|
||||||
|
archive_stop_on_violation = Type(bool, default = True)
|
@ -28,32 +28,19 @@ import sys
|
|||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from mkdocs import utils
|
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.plugins import BasePlugin, event_priority
|
from mkdocs.plugins import BasePlugin, event_priority
|
||||||
from mkdocs.structure.files import get_files
|
from mkdocs.structure.files import get_files
|
||||||
from pkg_resources import get_distribution, working_set
|
from pkg_resources import get_distribution, working_set
|
||||||
from zipfile import ZipFile, ZIP_DEFLATED
|
from zipfile import ZipFile, ZIP_DEFLATED
|
||||||
|
|
||||||
|
from material.plugins.info.config import InfoConfig
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# 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)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Info plugin
|
# Info plugin
|
||||||
class InfoPlugin(BasePlugin[InfoPluginConfig]):
|
class InfoPlugin(BasePlugin[InfoConfig]):
|
||||||
|
|
||||||
# Determine whether we're serving
|
# Determine whether we're serving
|
||||||
def on_startup(self, *, command, dirty):
|
def on_startup(self, *, command, dirty):
|
||||||
@ -235,5 +222,4 @@ def _size(value, factor = 1):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
log = logging.getLogger("mkdocs")
|
log = logging.getLogger("mkdocs.material.info")
|
||||||
log.addFilter(DuplicateFilter())
|
|
||||||
|
30
src/plugins/offline/config.py
Normal file
30
src/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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Offline plugin configuration scheme
|
||||||
|
class OfflineConfig(Config):
|
||||||
|
enabled = Type(bool, default = True)
|
@ -21,22 +21,16 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from mkdocs import utils
|
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.plugins import BasePlugin, event_priority
|
||||||
|
|
||||||
|
from material.plugins.offline.config import OfflineConfig
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# Class
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Offline plugin configuration scheme
|
|
||||||
class OfflinePluginConfig(Config):
|
|
||||||
enabled = opt.Type(bool, default = True)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Offline plugin
|
# Offline plugin
|
||||||
class OfflinePlugin(BasePlugin[OfflinePluginConfig]):
|
class OfflinePlugin(BasePlugin[OfflineConfig]):
|
||||||
|
|
||||||
# Initialize plugin
|
# Initialize plugin
|
||||||
def on_config(self, config):
|
def on_config(self, config):
|
||||||
|
51
src/plugins/search/config.py
Normal file
51
src/plugins/search/config.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Search pipeline functions
|
||||||
|
pipeline = ("stemmer", "stopWordFilter", "trimmer")
|
||||||
|
|
||||||
|
# Search plugin configuration scheme
|
||||||
|
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,34 +26,22 @@ import regex as re
|
|||||||
from html import escape
|
from html import escape
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from mkdocs import utils
|
from mkdocs import utils
|
||||||
from mkdocs.commands.build import DuplicateFilter
|
from mkdocs.config.config_options import SubConfig
|
||||||
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 mkdocs.plugins import BasePlugin
|
||||||
|
|
||||||
|
from material.plugins.search.config import SearchConfig, SearchFieldConfig
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jieba
|
||||||
|
except ImportError:
|
||||||
|
jieba = None
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# 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")
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Search plugin
|
# Search plugin
|
||||||
class SearchPlugin(BasePlugin[SearchPluginConfig]):
|
class SearchPlugin(BasePlugin[SearchConfig]):
|
||||||
|
|
||||||
# Determine whether we're running under dirty reload
|
# Determine whether we're running under dirty reload
|
||||||
def on_startup(self, *, command, dirty):
|
def on_startup(self, *, command, dirty):
|
||||||
@ -85,6 +73,30 @@ class SearchPlugin(BasePlugin[SearchPluginConfig]):
|
|||||||
# Initialize search index
|
# Initialize search index
|
||||||
self.search_index = SearchIndex(**self.config)
|
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.exists(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.exists(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
|
# Add page to search index
|
||||||
def on_page_context(self, context, *, page, config, nav):
|
def on_page_context(self, context, *, page, config, nav):
|
||||||
self.search_index.add_entry_from_context(page)
|
self.search_index.add_entry_from_context(page)
|
||||||
@ -167,9 +179,10 @@ class SearchIndex:
|
|||||||
title = "".join(section.title).strip()
|
title = "".join(section.title).strip()
|
||||||
text = "".join(section.text).strip()
|
text = "".join(section.text).strip()
|
||||||
|
|
||||||
# Reset text, if only titles should be indexed
|
# Segment Chinese characters if jieba is available
|
||||||
if self.config["indexing"] == "titles":
|
if jieba:
|
||||||
text = ""
|
title = self._segment_chinese(title)
|
||||||
|
text = self._segment_chinese(text)
|
||||||
|
|
||||||
# Create entry for section
|
# Create entry for section
|
||||||
entry = {
|
entry = {
|
||||||
@ -252,6 +265,25 @@ class SearchIndex:
|
|||||||
# No item found
|
# No item found
|
||||||
return None
|
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
|
# HTML element
|
||||||
@ -341,7 +373,8 @@ class Parser(HTMLParser):
|
|||||||
self.keep = set([
|
self.keep = set([
|
||||||
"p", # Paragraphs
|
"p", # Paragraphs
|
||||||
"code", "pre", # Code blocks
|
"code", "pre", # Code blocks
|
||||||
"li", "ol", "ul" # Lists
|
"li", "ol", "ul", # Lists
|
||||||
|
"sub", "sup" # Sub- and superscripts
|
||||||
])
|
])
|
||||||
|
|
||||||
# Current context and section
|
# Current context and section
|
||||||
@ -362,7 +395,7 @@ class Parser(HTMLParser):
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Handle headings
|
# Handle heading
|
||||||
if tag in ([f"h{x}" for x in range(1, 7)]):
|
if tag in ([f"h{x}" for x in range(1, 7)]):
|
||||||
depth = len(self.context)
|
depth = len(self.context)
|
||||||
if "id" in attrs:
|
if "id" in attrs:
|
||||||
@ -507,23 +540,22 @@ class Parser(HTMLParser):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
log = logging.getLogger("mkdocs")
|
log = logging.getLogger("mkdocs.material.search")
|
||||||
log.addFilter(DuplicateFilter())
|
|
||||||
|
|
||||||
# Tags that are self-closing
|
# Tags that are self-closing
|
||||||
void = set([
|
void = set([
|
||||||
"area", # Image map areas
|
"area", # Image map areas
|
||||||
"base", # Document base
|
"base", # Document base
|
||||||
"br", # Line breaks
|
"br", # Line breaks
|
||||||
"col", # Table columns
|
"col", # Table columns
|
||||||
"embed", # External content
|
"embed", # External content
|
||||||
"hr", # Horizontal rules
|
"hr", # Horizontal rules
|
||||||
"img", # Images
|
"img", # Images
|
||||||
"input", # Input fields
|
"input", # Input fields
|
||||||
"link", # Links
|
"link", # Links
|
||||||
"meta", # Metadata
|
"meta", # Metadata
|
||||||
"param", # External parameters
|
"param", # External parameters
|
||||||
"source", # Image source sets
|
"source", # Image source sets
|
||||||
"track", # Text track
|
"track", # Text track
|
||||||
"wbr" # Line break opportunities
|
"wbr" # Line break opportunities
|
||||||
])
|
])
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
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
src/plugins/social/config.py
Normal file
48
src/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
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Social plugin configuration scheme
|
||||||
|
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'"
|
||||||
|
)
|
@ -25,56 +25,26 @@ import os
|
|||||||
import posixpath
|
import posixpath
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
import sys
|
|
||||||
|
|
||||||
|
from cairosvg import svg2png
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from mkdocs.commands.build import DuplicateFilter
|
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 mkdocs.plugins import BasePlugin
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
try:
|
from material.plugins.social.config import SocialConfig
|
||||||
from cairosvg import svg2png
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
dependencies = True
|
|
||||||
except ImportError:
|
|
||||||
dependencies = False
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# 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'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Social plugin
|
# Social plugin
|
||||||
class SocialPlugin(BasePlugin[SocialPluginConfig]):
|
class SocialPlugin(BasePlugin[SocialConfig]):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._executor = concurrent.futures.ThreadPoolExecutor(4)
|
self._executor = concurrent.futures.ThreadPoolExecutor(4)
|
||||||
@ -104,14 +74,6 @@ class SocialPlugin(BasePlugin[SocialPluginConfig]):
|
|||||||
value = self.config.cards_font
|
value = self.config.cards_font
|
||||||
self.config.cards_layout_options["font_family"] = value
|
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
|
# Check if site URL is defined
|
||||||
if not config.site_url:
|
if not config.site_url:
|
||||||
log.warning(
|
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
src/plugins/tags/config.py
Normal file
43
src/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 material.plugins.tags import casefold
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Class
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Tags plugin configuration scheme
|
||||||
|
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 collections import defaultdict
|
||||||
from markdown.extensions.toc import slugify
|
from markdown.extensions.toc import slugify
|
||||||
from mkdocs import utils
|
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
|
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 material.plugins.tags import casefold
|
||||||
|
from material.plugins.tags.config import TagsConfig
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Class
|
# Class
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Tags plugin configuration scheme
|
|
||||||
class TagsPluginConfig(Config):
|
|
||||||
enabled = opt.Type(bool, default = True)
|
|
||||||
|
|
||||||
# Options for tags
|
|
||||||
tags_file = opt.Optional(opt.Type(str))
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Tags plugin
|
# Tags plugin
|
||||||
class TagsPlugin(BasePlugin[TagsPluginConfig]):
|
class TagsPlugin(BasePlugin[TagsConfig]):
|
||||||
supports_multiple_instances = True
|
supports_multiple_instances = True
|
||||||
|
|
||||||
# Initialize plugin
|
# Initialize plugin
|
||||||
@ -166,5 +159,4 @@ class TagsPlugin(BasePlugin[TagsPluginConfig]):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
log = logging.getLogger("mkdocs")
|
log = logging.getLogger("mkdocs.material.tags")
|
||||||
log.addFilter(DuplicateFilter())
|
|
||||||
|
Loading…
Reference in New Issue
Block a user