mkdocs-document-dates 3.7.0__tar.gz → 3.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mkdocs_document_dates-3.7.0/mkdocs_document_dates.egg-info → mkdocs_document_dates-3.7.1}/PKG-INFO +2 -2
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/cache_manager.py +33 -13
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/plugin.py +75 -72
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/.DS_Store +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/templates/recently_updated_group.html +128 -35
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/utils.py +169 -84
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1/mkdocs_document_dates.egg-info}/PKG-INFO +2 -2
- mkdocs_document_dates-3.7.1/mkdocs_document_dates.egg-info/requires.txt +1 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/pyproject.toml +5 -2
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/setup.py +1 -1
- mkdocs_document_dates-3.7.0/mkdocs_document_dates.egg-info/requires.txt +0 -1
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/LICENSE +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/MANIFEST.in +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/README.md +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/__init__.py +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/hooks/pre-commit +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/hooks_installer.py +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/config/user.config.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/config/user.config.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/core/core.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/core/core.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/core/default.config.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/core/md5.min.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/core/timeago.full.min.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/core/timeago.min.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/core/utils.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/fonts/material-icons.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/fonts/materialicons.woff2 +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/templates/recently_updated_detail.html +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/templates/recently_updated_grid.html +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/templates/recently_updated_list.html +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/tippy/backdrop.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/tippy/light.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/tippy/material.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/tippy/popper.min.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/tippy/scale.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/tippy/shift-away.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/tippy/tippy.css +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/tippy/tippy.umd.min.js +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates.egg-info/SOURCES.txt +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates.egg-info/dependency_links.txt +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates.egg-info/entry_points.txt +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates.egg-info/top_level.txt +0 -0
- {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/setup.cfg +0 -0
{mkdocs_document_dates-3.7.0/mkdocs_document_dates.egg-info → mkdocs_document_dates-3.7.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs-document-dates
|
|
3
|
-
Version: 3.7.
|
|
3
|
+
Version: 3.7.1
|
|
4
4
|
Summary: A new generation MkDocs plugin for displaying exact creation date, last updated date, authors, email of documents
|
|
5
5
|
Author-email: Aaron Wang <aaronwqt@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,7 +12,7 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.7
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: mkdocs
|
|
15
|
+
Requires-Dist: mkdocs<=1.6.1,>=1.6
|
|
16
16
|
Dynamic: license-file
|
|
17
17
|
|
|
18
18
|
# mkdocs-document-dates
|
{mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/cache_manager.py
RENAMED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import yaml
|
|
2
3
|
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from logging.handlers import RotatingFileHandler
|
|
6
7
|
from typing import Optional
|
|
7
|
-
from .utils import read_jsonl_cache, write_jsonl_cache,
|
|
8
|
+
from .utils import read_jsonl_cache, write_jsonl_cache, load_file_creation_date, load_git_first_commit_date
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger("mkdocs.plugins.document_dates")
|
|
10
11
|
_LOGGING_CONFIGURED = False
|
|
@@ -82,7 +83,8 @@ def _clean_git_env():
|
|
|
82
83
|
return env
|
|
83
84
|
|
|
84
85
|
def find_mkdocs_projects():
|
|
85
|
-
projects =
|
|
86
|
+
projects = set()
|
|
87
|
+
|
|
86
88
|
try:
|
|
87
89
|
git_root = Path(subprocess.check_output(
|
|
88
90
|
['git', 'rev-parse', '--show-toplevel'],
|
|
@@ -90,19 +92,20 @@ def find_mkdocs_projects():
|
|
|
90
92
|
encoding='utf-8'
|
|
91
93
|
).strip())
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
for config_file in git_root.rglob('
|
|
95
|
-
if config_file.name.lower() in
|
|
96
|
-
projects.
|
|
95
|
+
target_names = {'mkdocs.yml', 'properdocs.yml'}
|
|
96
|
+
for config_file in git_root.rglob('*.yml'):
|
|
97
|
+
if config_file.name.lower() in target_names:
|
|
98
|
+
projects.add(config_file.parent)
|
|
97
99
|
|
|
98
100
|
if not projects:
|
|
99
|
-
logger.warning("No MkDocs projects found in the repository")
|
|
101
|
+
logger.warning("No MkDocs/ProperDocs projects found in the repository")
|
|
102
|
+
|
|
100
103
|
except subprocess.CalledProcessError as e:
|
|
101
104
|
logger.error(f"Failed to find the Git repository root: {e}")
|
|
102
105
|
except Exception as e:
|
|
103
|
-
logger.error(f"Unexpected error while searching for
|
|
104
|
-
|
|
105
|
-
return projects
|
|
106
|
+
logger.error(f"Unexpected error while searching for projects: {e}")
|
|
107
|
+
|
|
108
|
+
return list(projects)
|
|
106
109
|
|
|
107
110
|
def setup_gitattributes(docs_dir: Path):
|
|
108
111
|
try:
|
|
@@ -136,8 +139,25 @@ def update_cache():
|
|
|
136
139
|
project_updated = False
|
|
137
140
|
|
|
138
141
|
docs_dir = project_dir / 'docs'
|
|
142
|
+
|
|
143
|
+
# 从 mkdocs.yml 中读取 docs_dir 配置覆盖默认值
|
|
144
|
+
try:
|
|
145
|
+
mkdocs_yml = project_dir / "properdocs.yml"
|
|
146
|
+
if not mkdocs_yml.exists():
|
|
147
|
+
mkdocs_yml = project_dir / "mkdocs.yml"
|
|
148
|
+
|
|
149
|
+
mkdocs_config = yaml.load(
|
|
150
|
+
mkdocs_yml.read_text(encoding="utf-8"),
|
|
151
|
+
Loader=yaml.BaseLoader,
|
|
152
|
+
) or {}
|
|
153
|
+
|
|
154
|
+
docs_dir_name = mkdocs_config.get("docs_dir") or "docs"
|
|
155
|
+
docs_dir = (project_dir / docs_dir_name).resolve(strict=False)
|
|
156
|
+
except (IOError, OSError, yaml.YAMLError) as e:
|
|
157
|
+
logger.warning(f"Failed to read docs_dir: {e}")
|
|
158
|
+
|
|
139
159
|
if not docs_dir.exists():
|
|
140
|
-
logger.
|
|
160
|
+
logger.info(f"Document directory does not exist: {docs_dir}")
|
|
141
161
|
continue
|
|
142
162
|
|
|
143
163
|
# 设置.gitattributes文件
|
|
@@ -165,9 +185,9 @@ def update_cache():
|
|
|
165
185
|
|
|
166
186
|
full_path = docs_dir / rel_path
|
|
167
187
|
if full_path.exists():
|
|
168
|
-
created_time =
|
|
188
|
+
created_time = load_file_creation_date(full_path).astimezone()
|
|
169
189
|
if not jsonl_cache_file.exists():
|
|
170
|
-
git_time =
|
|
190
|
+
git_time = load_git_first_commit_date(full_path)
|
|
171
191
|
if git_time:
|
|
172
192
|
created_time = min(created_time, git_time)
|
|
173
193
|
jsonl_dates_cache[rel_path] = {
|
|
@@ -2,15 +2,15 @@ import os
|
|
|
2
2
|
import yaml
|
|
3
3
|
import shutil
|
|
4
4
|
import logging
|
|
5
|
-
from jinja2 import
|
|
5
|
+
from jinja2 import ChoiceLoader, FileSystemLoader
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from mkdocs.plugins import BasePlugin
|
|
8
|
+
from mkdocs.plugins import BasePlugin, event_priority
|
|
9
9
|
from mkdocs.config import config_options
|
|
10
10
|
from mkdocs.structure.pages import Page
|
|
11
11
|
from mkdocs.utils import get_relative_url
|
|
12
12
|
from urllib.parse import urlparse
|
|
13
|
-
from .utils import
|
|
13
|
+
from .utils import load_file_creation_date, load_git_metadata, load_git_last_updated_dates, read_jsonl_cache, compile_exclude_patterns, is_excluded, get_recently_updated_files
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger("mkdocs.plugins.document_dates")
|
|
16
16
|
logger.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
@@ -44,7 +44,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
44
44
|
def __init__(self):
|
|
45
45
|
super().__init__()
|
|
46
46
|
|
|
47
|
-
self.
|
|
47
|
+
self.data_cached = {}
|
|
48
48
|
self.last_updated_dates = {}
|
|
49
49
|
self.authors_yml = {}
|
|
50
50
|
self.recent_docs_html = None
|
|
@@ -58,25 +58,25 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
58
58
|
authors_file = docs_dir_path / 'authors.yml'
|
|
59
59
|
if not authors_file.exists():
|
|
60
60
|
try:
|
|
61
|
-
blog_config = config
|
|
61
|
+
blog_config = config.plugins.get(f"{config.theme.name}/blog").config
|
|
62
62
|
authors_file_resolved = blog_config.authors_file.format(blog=blog_config.blog_dir)
|
|
63
63
|
authors_file = docs_dir_path / authors_file_resolved
|
|
64
64
|
except Exception:
|
|
65
65
|
pass
|
|
66
66
|
self._load_authors_from_yaml(authors_file)
|
|
67
67
|
|
|
68
|
-
#
|
|
69
|
-
self.
|
|
70
|
-
#
|
|
68
|
+
# 加载文档 git 元数据(日期 & 作者)
|
|
69
|
+
self.data_cached = load_git_metadata(docs_dir_path)
|
|
70
|
+
# 加载 jsonl 缓存数据
|
|
71
71
|
jsonl_cache_file = docs_dir_path / '.dates_cache.jsonl'
|
|
72
72
|
if jsonl_cache_file.exists():
|
|
73
73
|
jsonl_cache = read_jsonl_cache(jsonl_cache_file)
|
|
74
74
|
for filename, new_info in jsonl_cache.items():
|
|
75
|
-
if filename in self.
|
|
76
|
-
self.
|
|
75
|
+
if filename in self.data_cached:
|
|
76
|
+
self.data_cached[filename].update(new_info)
|
|
77
77
|
|
|
78
|
-
# 加载文档最近更新时间
|
|
79
|
-
self.last_updated_dates =
|
|
78
|
+
# 加载文档最近更新时间(日期)
|
|
79
|
+
self.last_updated_dates = load_git_last_updated_dates(docs_dir_path)
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
# 复制配置文件到用户目录(如果不存在)
|
|
@@ -151,6 +151,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
151
151
|
|
|
152
152
|
return config
|
|
153
153
|
|
|
154
|
+
@event_priority(50)
|
|
154
155
|
def on_page_markdown(self, markdown, page: Page, config, files):
|
|
155
156
|
# 获取相对路径,src_uri 总是以"/"分隔
|
|
156
157
|
rel_path = getattr(page.file, 'src_uri', page.file.src_path)
|
|
@@ -158,20 +159,21 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
158
159
|
rel_path = rel_path.replace(os.sep, '/')
|
|
159
160
|
file_path = page.file.abs_src_path
|
|
160
161
|
|
|
161
|
-
#
|
|
162
|
-
created = self.
|
|
163
|
-
updated = self.
|
|
162
|
+
# 优先获取 page.meta 中的数据
|
|
163
|
+
created = self._load_meta_date(page.meta, self.config['created_field_names'])
|
|
164
|
+
updated = self._load_meta_date(page.meta, self.config['updated_field_names'])
|
|
165
|
+
authors = self._load_meta_author(page.meta, page.url)
|
|
166
|
+
|
|
167
|
+
# 再获取缓存的数据
|
|
164
168
|
if not created:
|
|
165
|
-
created = self.
|
|
169
|
+
created = self._load_created_cached(file_path, rel_path)
|
|
166
170
|
if not updated:
|
|
167
|
-
updated = self.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
authors = self._get_author_info(rel_path, page, config)
|
|
171
|
+
updated = self._load_updated_cached(file_path, rel_path)
|
|
172
|
+
if not authors:
|
|
173
|
+
authors = self._load_author_cached(rel_path, page, config)
|
|
171
174
|
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
mx["document_dates"] = {
|
|
175
|
+
# 注入数据
|
|
176
|
+
page.meta["document_dates"] = {
|
|
175
177
|
"dates": {
|
|
176
178
|
"created": created.isoformat(),
|
|
177
179
|
"updated": updated.isoformat(),
|
|
@@ -189,6 +191,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
189
191
|
# 将信息写入 markdown
|
|
190
192
|
return self._insert_date_info(markdown, info_html)
|
|
191
193
|
|
|
194
|
+
@event_priority(50)
|
|
192
195
|
def on_env(self, env, config, files):
|
|
193
196
|
recently_updated_config = self.config.get('recently-updated')
|
|
194
197
|
if recently_updated_config:
|
|
@@ -207,26 +210,23 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
207
210
|
recently_updated_docs = get_recently_updated_files(self.last_updated_dates, files, recent_exclude_patterns, limit, self.recent_enable)
|
|
208
211
|
|
|
209
212
|
# 将数据注入到 config['extra'] 中供全局访问
|
|
210
|
-
if 'extra'
|
|
211
|
-
config['extra'] =
|
|
212
|
-
config['extra']['recently_updated_docs'] = recently_updated_docs
|
|
213
|
+
if not config.get('extra', {}).get("recently_updated_docs", {}):
|
|
214
|
+
config['extra']['recently_updated_docs'] = recently_updated_docs
|
|
213
215
|
|
|
214
216
|
# 渲染HTML
|
|
215
217
|
if self.recent_enable:
|
|
216
|
-
|
|
218
|
+
# 摘要行数的动态配置
|
|
219
|
+
summary = recently_updated_config.get("summary_lines", {})
|
|
220
|
+
summary_lines = {
|
|
221
|
+
"grid": summary.get("grid", 4),
|
|
222
|
+
"detail": summary.get("detail", 6),
|
|
223
|
+
}
|
|
217
224
|
|
|
218
|
-
|
|
219
|
-
# def mdd_access(page, domain):
|
|
220
|
-
# return (
|
|
221
|
-
# page.meta.get("_mx", {})
|
|
222
|
-
# .get("document_dates", {})
|
|
223
|
-
# .get(domain, {})
|
|
224
|
-
# )
|
|
225
|
-
|
|
226
|
-
# env.globals["mdd"] = mdd_access
|
|
225
|
+
self.recent_docs_html = self._render_recently_updated_html(env, config, recently_updated_docs, summary_lines)
|
|
227
226
|
|
|
228
227
|
return env
|
|
229
228
|
|
|
229
|
+
@event_priority(50)
|
|
230
230
|
def on_post_page(self, output, page, config):
|
|
231
231
|
if self.recent_enable and '\n<!-- RECENTLY_UPDATED_DOCS -->' in output:
|
|
232
232
|
output = output.replace('\n<!-- RECENTLY_UPDATED_DOCS -->', self.recent_docs_html or '')
|
|
@@ -257,23 +257,31 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
257
257
|
logger.info(f"Error parsing .authors.yml: {e}")
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def _render_recently_updated_html(self, recently_updated_data):
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
def _render_recently_updated_html(self, env, config, recently_updated_data, summary_lines):
|
|
261
|
+
# 设置模板加载器
|
|
262
|
+
template_path = Path(__file__).parent / 'static' / 'templates'
|
|
263
|
+
env.loader = ChoiceLoader([
|
|
264
|
+
FileSystemLoader(str(template_path)),
|
|
265
|
+
env.loader
|
|
266
|
+
])
|
|
264
267
|
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
# 用于模板的安全阀
|
|
269
|
+
try:
|
|
270
|
+
env.get_template("partials/language.html")
|
|
271
|
+
env.globals["HAS_LANGUAGE_TEMPLATE"] = True
|
|
272
|
+
except Exception:
|
|
273
|
+
env.globals["HAS_LANGUAGE_TEMPLATE"] = False
|
|
274
|
+
|
|
275
|
+
# 获取模板并渲染
|
|
276
|
+
template = env.get_template("recently_updated_group.html")
|
|
277
|
+
return template.render(
|
|
278
|
+
recent_docs=recently_updated_data,
|
|
279
|
+
summary_lines=summary_lines,
|
|
280
|
+
config=config
|
|
269
281
|
)
|
|
270
|
-
template = env.get_template(template_file)
|
|
271
282
|
|
|
272
|
-
# 渲染模板
|
|
273
|
-
return template.render(recent_docs=recently_updated_data)
|
|
274
283
|
|
|
275
|
-
|
|
276
|
-
def _find_meta_date(self, meta, field_names):
|
|
284
|
+
def _load_meta_date(self, meta, field_names):
|
|
277
285
|
for field in field_names:
|
|
278
286
|
if field in meta:
|
|
279
287
|
try:
|
|
@@ -284,14 +292,14 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
284
292
|
continue
|
|
285
293
|
return None
|
|
286
294
|
|
|
287
|
-
def
|
|
295
|
+
def _load_created_cached(self, file_path, rel_path):
|
|
288
296
|
# 优先从缓存中读取
|
|
289
|
-
if rel_path in self.
|
|
290
|
-
return datetime.fromisoformat(self.
|
|
297
|
+
if rel_path in self.data_cached:
|
|
298
|
+
return datetime.fromisoformat(self.data_cached[rel_path]['created'])
|
|
291
299
|
# 从文件系统获取
|
|
292
|
-
return
|
|
300
|
+
return load_file_creation_date(file_path).astimezone()
|
|
293
301
|
|
|
294
|
-
def
|
|
302
|
+
def _load_updated_cached(self, file_path, rel_path):
|
|
295
303
|
# 优先从缓存中读取
|
|
296
304
|
if rel_path in self.last_updated_dates:
|
|
297
305
|
return datetime.fromtimestamp(self.last_updated_dates[rel_path]).astimezone()
|
|
@@ -300,34 +308,29 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
300
308
|
return datetime.fromtimestamp(stat.st_mtime).astimezone()
|
|
301
309
|
|
|
302
310
|
|
|
303
|
-
def
|
|
304
|
-
# 1.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return authors
|
|
308
|
-
|
|
309
|
-
# 2. git author
|
|
310
|
-
if rel_path in self.dates_cache:
|
|
311
|
-
authors_list = self.dates_cache[rel_path].get('authors')
|
|
311
|
+
def _load_author_cached(self, rel_path, page, config):
|
|
312
|
+
# 1. git author
|
|
313
|
+
if rel_path in self.data_cached:
|
|
314
|
+
authors_list = self.data_cached[rel_path].get('authors')
|
|
312
315
|
if authors_list:
|
|
313
316
|
authors = []
|
|
314
317
|
for data in authors_list:
|
|
315
318
|
full_author = self.authors_yml.get(data['name'])
|
|
316
319
|
if full_author:
|
|
317
|
-
authors.append(self.
|
|
320
|
+
authors.append(self._repair_author(full_author, page.url))
|
|
318
321
|
else:
|
|
319
322
|
authors.append(Author(**data))
|
|
320
323
|
return authors
|
|
321
324
|
|
|
322
|
-
#
|
|
325
|
+
# 2. site_author 或 PC username
|
|
323
326
|
name = config.get('site_author') or Path.home().name
|
|
324
327
|
full_author = self.authors_yml.get(name)
|
|
325
328
|
if full_author:
|
|
326
|
-
return [self.
|
|
329
|
+
return [self._repair_author(full_author, page.url)]
|
|
327
330
|
else:
|
|
328
331
|
return [Author(name=name)]
|
|
329
332
|
|
|
330
|
-
def
|
|
333
|
+
def _load_meta_author(self, meta, page_url):
|
|
331
334
|
try:
|
|
332
335
|
# 匹配 authors 数组
|
|
333
336
|
author_objs = []
|
|
@@ -335,7 +338,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
335
338
|
for key in authors_data or []:
|
|
336
339
|
full_author = self.authors_yml.get(key)
|
|
337
340
|
if full_author:
|
|
338
|
-
author_objs.append(self.
|
|
341
|
+
author_objs.append(self._repair_author(full_author, page_url))
|
|
339
342
|
else:
|
|
340
343
|
author_objs.append(Author(name=str(key)))
|
|
341
344
|
if author_objs:
|
|
@@ -349,14 +352,14 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
349
352
|
name = email.partition('@')[0]
|
|
350
353
|
full_author = self.authors_yml.get(name)
|
|
351
354
|
if full_author:
|
|
352
|
-
return [self.
|
|
355
|
+
return [self._repair_author(full_author, page_url)]
|
|
353
356
|
else:
|
|
354
357
|
return [Author(name=name, email=email)]
|
|
355
358
|
except Exception as e:
|
|
356
359
|
logger.warning(f"Error processing author meta: {e}")
|
|
357
360
|
return None
|
|
358
361
|
|
|
359
|
-
def
|
|
362
|
+
def _repair_author(self, author: Author, page_url: str) -> Author:
|
|
360
363
|
try:
|
|
361
364
|
if not author.avatar:
|
|
362
365
|
return author
|
|
@@ -370,7 +373,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
370
373
|
return author
|
|
371
374
|
|
|
372
375
|
|
|
373
|
-
def
|
|
376
|
+
def _formatting_date(self, date: datetime):
|
|
374
377
|
if self.config['type'] == 'timeago':
|
|
375
378
|
return ""
|
|
376
379
|
elif self.config['type'] == 'datetime':
|
|
@@ -399,7 +402,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
|
399
402
|
return (
|
|
400
403
|
f"<span class='dd-item' data-tippy-content data-tippy-raw='{formatted}'>"
|
|
401
404
|
f"<span class='material-icons' data-icon='{icon}'></span>"
|
|
402
|
-
f"<time datetime='{time_obj.isoformat()}'>{self.
|
|
405
|
+
f"<time datetime='{time_obj.isoformat()}'>{self._formatting_date(time_obj)}</time>"
|
|
403
406
|
f"</span>"
|
|
404
407
|
)
|
|
405
408
|
|
{mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/static/.DS_Store
RENAMED
|
Binary file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<style>
|
|
2
|
+
/* --- 视图选择器 --- */
|
|
2
3
|
.article-layout-switcher {
|
|
3
4
|
display: flex;
|
|
4
5
|
justify-content: flex-end;
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
font-size: 20px;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/* ---
|
|
36
|
+
/* --- 三种视图 --- */
|
|
36
37
|
.article-grid {
|
|
37
38
|
display: grid;
|
|
38
39
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
@@ -40,15 +41,18 @@
|
|
|
40
41
|
margin: 20px 0;
|
|
41
42
|
/* transition: all 0.2s ease; */
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
/* --- 详情模式 (is-detail) --- */
|
|
45
44
|
.article-grid.is-detail {
|
|
46
|
-
grid-template-columns: 1fr;
|
|
45
|
+
grid-template-columns: minmax(0, 1fr);
|
|
47
46
|
margin-left: auto;
|
|
48
47
|
margin-right: auto;
|
|
49
48
|
}
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
.article-grid.is-detail .card-summary {
|
|
50
|
+
--summary-lines: var(--summary-lines-detail);
|
|
51
|
+
}
|
|
52
|
+
.article-grid.is-detail .card-cover {
|
|
53
|
+
width: 135px;
|
|
54
|
+
height: 90px;
|
|
55
|
+
}
|
|
52
56
|
.article-grid.is-list {
|
|
53
57
|
margin-left: auto;
|
|
54
58
|
margin-right: auto;
|
|
@@ -61,6 +65,9 @@
|
|
|
61
65
|
.article-grid.is-list .card-body {
|
|
62
66
|
display: none;
|
|
63
67
|
}
|
|
68
|
+
.article-grid.is-list .card-footer {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
64
71
|
.article-grid.is-list .article-card {
|
|
65
72
|
border: none;
|
|
66
73
|
border-radius: 0;
|
|
@@ -79,7 +86,7 @@
|
|
|
79
86
|
margin-right: 0;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
/* --- Card --- */
|
|
83
90
|
.article-card {
|
|
84
91
|
border: 1px solid rgba(142, 142, 142, 0.2);
|
|
85
92
|
border-radius: 8px;
|
|
@@ -88,13 +95,14 @@
|
|
|
88
95
|
box-sizing: border-box;
|
|
89
96
|
display: flex;
|
|
90
97
|
flex-direction: column;
|
|
98
|
+
height:100%;
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
/* ---
|
|
101
|
+
/* --- Header --- */
|
|
94
102
|
.card-header {
|
|
95
103
|
display: flex;
|
|
96
104
|
justify-content: space-between;
|
|
97
|
-
align-items:
|
|
105
|
+
align-items: baseline;
|
|
98
106
|
text-decoration: none;
|
|
99
107
|
}
|
|
100
108
|
.card-title {
|
|
@@ -104,10 +112,12 @@
|
|
|
104
112
|
margin-right: 12px;
|
|
105
113
|
}
|
|
106
114
|
.card-date {
|
|
107
|
-
font-size: 0.
|
|
108
|
-
color: rgba(100, 100, 100, 0.
|
|
115
|
+
font-size: 0.8em;
|
|
116
|
+
color: rgba(100, 100, 100, 0.6);
|
|
109
117
|
white-space: nowrap;
|
|
110
118
|
margin-right: 10px;
|
|
119
|
+
/* 补偿 baseline 布局时字体度量的误差 */
|
|
120
|
+
transform: translateY(-1px);
|
|
111
121
|
}
|
|
112
122
|
.card-header:hover,
|
|
113
123
|
.card-header:focus {
|
|
@@ -118,37 +128,45 @@
|
|
|
118
128
|
text-decoration: underline;
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
/* ---
|
|
131
|
+
/* --- Body --- */
|
|
122
132
|
.card-body {
|
|
123
|
-
display: flex;
|
|
124
|
-
justify-content: space-between;
|
|
125
|
-
align-items: center;
|
|
126
133
|
margin-top: 12px;
|
|
127
134
|
}
|
|
128
135
|
.card-summary {
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
--summary-lines: var(--summary-lines-grid, 4);
|
|
137
|
+
--lh: 1.8em;
|
|
138
|
+
|
|
139
|
+
line-height: var(--lh);
|
|
140
|
+
max-height: calc(var(--summary-lines) * var(--lh));
|
|
141
|
+
|
|
142
|
+
position: relative;
|
|
143
|
+
overflow: visible;
|
|
144
|
+
pointer-events: none;
|
|
145
|
+
|
|
146
|
+
color: var(--md-default-fg-color--light, rgb(121, 121, 121));
|
|
131
147
|
font-size: 0.7rem;
|
|
132
|
-
line-height: 1.6;
|
|
133
148
|
letter-spacing: 0.02rem;
|
|
134
149
|
word-break: break-all;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
|
|
151
|
+
-webkit-mask-image: linear-gradient(
|
|
152
|
+
to bottom,
|
|
153
|
+
black calc(100% - var(--lh)*1.2),
|
|
154
|
+
transparent 100%
|
|
155
|
+
);
|
|
156
|
+
mask-image: linear-gradient(
|
|
157
|
+
to bottom,
|
|
158
|
+
black calc(100% - var(--lh)*1.2),
|
|
159
|
+
transparent 100%
|
|
160
|
+
);
|
|
141
161
|
}
|
|
142
162
|
.card-cover {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
align-items: center;
|
|
149
|
-
justify-content: center;
|
|
163
|
+
float: right;
|
|
164
|
+
width: 105px;
|
|
165
|
+
height: 70px;
|
|
166
|
+
margin: 2px 0 0 10px;
|
|
167
|
+
border-radius: 4px;
|
|
150
168
|
overflow: hidden;
|
|
151
|
-
|
|
169
|
+
|
|
152
170
|
transition: filter 0.3s ease;
|
|
153
171
|
|
|
154
172
|
/* Safari 圆角闪烁修复 */
|
|
@@ -181,6 +199,55 @@
|
|
|
181
199
|
.card-cover:hover img {
|
|
182
200
|
transform: scale(1.1) translateX(4px) translateZ(0);
|
|
183
201
|
}
|
|
202
|
+
.card-body::after {
|
|
203
|
+
content: "";
|
|
204
|
+
display: block;
|
|
205
|
+
clear: both;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* --- Footer --- */
|
|
209
|
+
.card-footer {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
margin-top: auto;
|
|
213
|
+
padding-top: 12px;
|
|
214
|
+
margin-bottom: -4px;
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
line-height: 1.6;
|
|
217
|
+
}
|
|
218
|
+
.card-footer .read-time {
|
|
219
|
+
display: inline-flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
font-size: 0.65rem;
|
|
222
|
+
color: rgba(100, 100, 100, 0.6);
|
|
223
|
+
white-space: nowrap;
|
|
224
|
+
margin-right: 12px;
|
|
225
|
+
}
|
|
226
|
+
.card-footer .read-time-icon {
|
|
227
|
+
font-size: 0.8rem;
|
|
228
|
+
opacity: .7;
|
|
229
|
+
margin-right: 4px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.md-typeset .md-tags:not([hidden]) {
|
|
233
|
+
margin: 0 0 0 auto;
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
min-width: 0;
|
|
237
|
+
gap: 0;
|
|
238
|
+
flex-wrap: nowrap;
|
|
239
|
+
overflow: hidden;
|
|
240
|
+
}
|
|
241
|
+
.md-typeset .md-tag {
|
|
242
|
+
font-weight: 400;
|
|
243
|
+
flex-shrink: 0;
|
|
244
|
+
gap: 0;
|
|
245
|
+
color: var(--md-default-fg-color--light);
|
|
246
|
+
margin-right: 6px;
|
|
247
|
+
}
|
|
248
|
+
.md-typeset .md-tag-icon::before {
|
|
249
|
+
margin-right: 4px;
|
|
250
|
+
}
|
|
184
251
|
</style>
|
|
185
252
|
|
|
186
253
|
<div class="article-layout-switcher">
|
|
@@ -189,21 +256,47 @@
|
|
|
189
256
|
<button class="layout-grid-btn"><span class="material-icons">view_module</span></button>
|
|
190
257
|
</div>
|
|
191
258
|
|
|
192
|
-
<div class="article-grid"
|
|
193
|
-
|
|
259
|
+
<div class="article-grid"
|
|
260
|
+
style="
|
|
261
|
+
--summary-lines-grid: {{ summary_lines.grid }};
|
|
262
|
+
--summary-lines-detail: {{ summary_lines.detail }};
|
|
263
|
+
">
|
|
264
|
+
{%- for mtime, rel_path, title, url, cover, summary, readtime, tags in recent_docs %}
|
|
194
265
|
<div class="article-card">
|
|
195
266
|
<a class="card-header" href="{{ url }}" target="_blank">
|
|
196
267
|
<div class="card-title">{{ title }}</div>
|
|
197
268
|
<time class="dd-timeago card-date" datetime="{{ mtime }}">{{ mtime[:10] }}</time>
|
|
198
269
|
</a>
|
|
199
270
|
<div class="card-body">
|
|
200
|
-
<div class="card-summary">{{ summary }}</div>
|
|
201
271
|
{%- if cover %}
|
|
202
272
|
<div class="card-cover">
|
|
203
273
|
<img src="{{ cover }}" alt="{{ title }}">
|
|
204
274
|
</div>
|
|
205
275
|
{%- endif %}
|
|
276
|
+
<div class="card-summary">{{ summary }}</div>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
{%- if tags or readtime > 0 %}
|
|
280
|
+
<div class="card-footer">
|
|
281
|
+
{%- if readtime > 0 %}
|
|
282
|
+
<span class="read-time"><span class="read-time-icon material-icons">hourglass_top</span>
|
|
283
|
+
{%- if HAS_LANGUAGE_TEMPLATE -%}
|
|
284
|
+
{%- import "partials/language.html" as lang with context -%}
|
|
285
|
+
{{ lang.t("readtime.other") | replace("#", readtime) }}
|
|
286
|
+
{%- else -%}
|
|
287
|
+
{{ readtime }} min read
|
|
288
|
+
{%- endif -%}
|
|
289
|
+
</span>
|
|
290
|
+
{%- endif %}
|
|
291
|
+
{%- if tags %}
|
|
292
|
+
<div class="md-tags">
|
|
293
|
+
{%- for tag in tags %}
|
|
294
|
+
<span class="md-tag md-tag-icon">{{- tag -}}</span>
|
|
295
|
+
{%- endfor %}
|
|
296
|
+
</div>
|
|
297
|
+
{%- endif %}
|
|
206
298
|
</div>
|
|
299
|
+
{%- endif %}
|
|
207
300
|
</div>
|
|
208
301
|
{%- endfor %}
|
|
209
302
|
</div>
|
|
@@ -6,6 +6,7 @@ import logging
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import fnmatch
|
|
8
8
|
import re
|
|
9
|
+
import math
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from collections import defaultdict
|
|
@@ -34,7 +35,7 @@ def is_excluded(path, patterns):
|
|
|
34
35
|
return True
|
|
35
36
|
return False
|
|
36
37
|
|
|
37
|
-
def
|
|
38
|
+
def load_file_creation_date(file_path):
|
|
38
39
|
try:
|
|
39
40
|
stat = os.stat(file_path)
|
|
40
41
|
system = platform.system().lower()
|
|
@@ -48,10 +49,10 @@ def get_file_creation_time(file_path):
|
|
|
48
49
|
else: # Linux, 没有创建时间,使用修改时间
|
|
49
50
|
return datetime.fromtimestamp(stat.st_mtime)
|
|
50
51
|
except (OSError, ValueError) as e:
|
|
51
|
-
logger.error(f"Failed to
|
|
52
|
+
logger.error(f"Failed to load file creation date for {file_path}: {e}")
|
|
52
53
|
return datetime.now()
|
|
53
54
|
|
|
54
|
-
def
|
|
55
|
+
def load_git_first_commit_date(file_path):
|
|
55
56
|
try:
|
|
56
57
|
# git log --reverse --format="%aI" -- {file_path} | head -n 1
|
|
57
58
|
cmd_list = ['git', 'log', '--reverse', '--format=%aI', '--', file_path]
|
|
@@ -60,7 +61,7 @@ def get_git_first_commit_time(file_path):
|
|
|
60
61
|
first_line = process.stdout.partition('\n')[0].strip()
|
|
61
62
|
return datetime.fromisoformat(first_line)
|
|
62
63
|
except Exception as e:
|
|
63
|
-
logger.info(f"Error
|
|
64
|
+
logger.info(f"Error load git first commit date for {file_path}: {e}")
|
|
64
65
|
return None
|
|
65
66
|
|
|
66
67
|
def load_git_metadata(docs_dir_path: Path):
|
|
@@ -107,7 +108,7 @@ def load_git_metadata(docs_dir_path: Path):
|
|
|
107
108
|
logger.info(f"Error getting git info in {docs_dir_path}: {e}")
|
|
108
109
|
return dates_cache
|
|
109
110
|
|
|
110
|
-
def
|
|
111
|
+
def load_git_last_updated_dates(docs_dir_path: Path):
|
|
111
112
|
doc_mtime_map = {}
|
|
112
113
|
try:
|
|
113
114
|
git_root = Path(subprocess.check_output(
|
|
@@ -160,21 +161,26 @@ def get_recently_updated_files(existing_dates: dict, files: Files, exclude_list:
|
|
|
160
161
|
# 优先从现有数据获取 mtime,如果不存在则 fallback 到文件系统 mtime
|
|
161
162
|
mtime = existing_dates.get(rel_path, os.path.getmtime(file.abs_src_path))
|
|
162
163
|
|
|
163
|
-
#
|
|
164
|
+
# 获取文档其它信息
|
|
164
165
|
title = file.page.title if file.page and file.page.title else file.name
|
|
165
166
|
url = file.page.url if file.page and file.page.url else file.url
|
|
167
|
+
tags = file.page.meta.get("tags") or []
|
|
166
168
|
|
|
167
169
|
cover = ''
|
|
168
170
|
summary = ''
|
|
171
|
+
readtime = 0
|
|
169
172
|
# authors = []
|
|
170
173
|
if file.page:
|
|
171
174
|
cover = file.page.meta.get('cover', '')
|
|
172
|
-
# authors = file.page.meta.
|
|
175
|
+
# authors = file.page.meta.document_dates.authors
|
|
173
176
|
if file.page.file:
|
|
174
|
-
summary =
|
|
177
|
+
summary, readtime = analyze_markdown(file.page.file.content_string)
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
meta_readtime = int(file.page.meta.get('readtime') or 0)
|
|
180
|
+
readtime = meta_readtime if meta_readtime > 0 else readtime
|
|
181
|
+
|
|
182
|
+
# 存储信息(更新时间、路径、标题、URL、封面、摘要、阅读时间、标签)
|
|
183
|
+
files_meta.append((mtime, rel_path, title, url, cover, summary, readtime, tags))
|
|
178
184
|
# existing_map[rel_path] = mtime
|
|
179
185
|
|
|
180
186
|
# 构建最近更新列表
|
|
@@ -229,50 +235,108 @@ def write_jsonl_cache(jsonl_file: Path, dates_cache, tracked_files):
|
|
|
229
235
|
return False
|
|
230
236
|
|
|
231
237
|
|
|
238
|
+
# ==================================================
|
|
239
|
+
# High-performance Readtime & Summary parser design:
|
|
240
|
+
#
|
|
241
|
+
# - O(n) single-pass parser (scan once)
|
|
242
|
+
# - No AST construction
|
|
243
|
+
# - Finite state machine
|
|
244
|
+
# - Block detection: frontmatter / fence / HTML / math / comment
|
|
245
|
+
# - Inline and block parsing separated
|
|
246
|
+
# - Summary and read-time computed in the same pass
|
|
247
|
+
#
|
|
248
|
+
# Language support:
|
|
249
|
+
#
|
|
250
|
+
# - CJK languages: Chinese, Japanese, Korean
|
|
251
|
+
# - Space-delimited languages: English, Spanish, French, German, Portuguese, Russian ...
|
|
252
|
+
# - Supports mixed-language content (e.g. English + CJK)
|
|
253
|
+
# ==================================================
|
|
254
|
+
|
|
255
|
+
# ===== Extract Readtime =====
|
|
256
|
+
DEFAULT_WPM = 240
|
|
257
|
+
|
|
258
|
+
# Match Unicode "words" for space-delimited languages (English, Spanish, French, German, Russian, etc.)
|
|
259
|
+
# CJK characters also match \w in Python, so they are removed before applying this regex to avoid double counting
|
|
260
|
+
WORD_RE = re.compile(r"\w+", re.UNICODE)
|
|
261
|
+
# WORD_RE = re.compile(r"[A-Za-z0-9_']+")
|
|
262
|
+
|
|
263
|
+
# Match common CJK characters (Chinese, Japanese, Korean).
|
|
264
|
+
# These languages do not use spaces between words, so characters are counted separately and weighted differently in Readtime
|
|
265
|
+
# Ranges:
|
|
266
|
+
# \u4E00–\u9FFF : Chinese (both Simplified and Traditional)
|
|
267
|
+
# \u3040–\u30FF : Japanese Hiragana and Katakana
|
|
268
|
+
# \uAC00–\uD7AF : Korean Hangul syllables
|
|
269
|
+
CJK_RE = re.compile(r"[\u4E00-\u9FFF\u3040-\u30FF\uAC00-\uD7AF]")
|
|
270
|
+
|
|
271
|
+
|
|
232
272
|
# ===== Extract Summary =====
|
|
233
273
|
#
|
|
234
274
|
# -------- block skip --------
|
|
235
|
-
# Fence
|
|
236
275
|
FENCE_RE = re.compile(r"^\s*([`~]{3,})")
|
|
237
276
|
|
|
238
|
-
# HTML comment
|
|
239
|
-
HTML_COMMENT_START = re.compile(r'<!--', re.I)
|
|
240
|
-
HTML_COMMENT_END = re.compile(r'-->', re.I)
|
|
241
|
-
|
|
242
277
|
# HTML
|
|
243
|
-
HTML_TAG_OPEN = re.compile(r
|
|
244
|
-
|
|
278
|
+
HTML_TAG_OPEN = re.compile(r"<\s*([a-zA-Z][\w\-]*)\b", re.I)
|
|
279
|
+
HTML_VALID_TAGS = {
|
|
280
|
+
"html","head","title","base","link","meta","style",
|
|
281
|
+
"body","article","section","nav","aside","header","footer","main",
|
|
282
|
+
"h1","h2","h3","h4","h5","h6",
|
|
283
|
+
"p","hr","pre","blockquote","ol","ul","li","dl","dt","dd",
|
|
284
|
+
"figure","figcaption","div",
|
|
285
|
+
"a","em","strong","small","s","cite","q","dfn","abbr","data","time",
|
|
286
|
+
"code","var","samp","kbd","sub","sup","i","b","u","mark",
|
|
287
|
+
"ruby","rt","rp","bdi","bdo","span","br","wbr",
|
|
288
|
+
"ins","del",
|
|
289
|
+
"picture","source","img","iframe","embed","object","param",
|
|
290
|
+
"video","audio","track","map","area",
|
|
291
|
+
"svg","math",
|
|
292
|
+
"table","caption","colgroup","col","tbody","thead","tfoot",
|
|
293
|
+
"tr","td","th",
|
|
294
|
+
"form","label","input","button","select","datalist","optgroup",
|
|
295
|
+
"option","textarea","output","progress","meter",
|
|
296
|
+
"fieldset","legend",
|
|
297
|
+
"details","summary","dialog",
|
|
298
|
+
"script","noscript","template","slot","canvas",
|
|
299
|
+
"font","center","big","tt","strike","basefont","dir","applet"
|
|
300
|
+
}
|
|
245
301
|
HTML_VOID_TAGS = {
|
|
246
302
|
"area", "base", "br", "col", "embed", "hr",
|
|
247
303
|
"img", "input", "link", "meta", "param",
|
|
248
304
|
"source", "track", "wbr"
|
|
249
305
|
}
|
|
250
|
-
HTML_VOID_CLOSE_RE = re.compile(r">", re.I)
|
|
251
306
|
|
|
252
307
|
# -------- inline skip --------
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
308
|
+
# TABLE_ROW_RE = re.compile(r"^\s*\|.*\|\s*$")
|
|
309
|
+
REF_LINK_RE = re.compile(r"^\s*\[.+?\]:")
|
|
310
|
+
H1_TITLE = re.compile(r"^\s*# .+$", re.MULTILINE)
|
|
311
|
+
SINGLE_LINE_HTML_NOISE = re.compile(r"^</?[a-z][\w-]*[^>]*>", re.I)
|
|
312
|
+
def inline_skip(line: str):
|
|
313
|
+
s = line.lstrip()
|
|
314
|
+
if s.startswith(">"):
|
|
315
|
+
return True
|
|
316
|
+
if s.startswith("!!!") or s.startswith("???"):
|
|
317
|
+
return True
|
|
318
|
+
if s.startswith("==="):
|
|
319
|
+
return True
|
|
320
|
+
return False
|
|
265
321
|
|
|
266
322
|
# -------- inline replace --------
|
|
267
|
-
IMAGE_RE = re.compile(r
|
|
268
|
-
LINK_RE = re.compile(r
|
|
323
|
+
IMAGE_RE = re.compile(r"!\[[^\]]*\]\([^)]+\)")
|
|
324
|
+
LINK_RE = re.compile(r"\[([^\]]+)\]\([^)]+\)")
|
|
269
325
|
BRACE_RE = re.compile(r"\{[^}]*\}")
|
|
270
|
-
MD_SYNTAX_RE = re.compile(r
|
|
326
|
+
MD_SYNTAX_RE = re.compile(r"[`*_>#]+")
|
|
271
327
|
|
|
272
|
-
def clean_markdown(md: str) -> list:
|
|
273
328
|
|
|
274
|
-
|
|
275
|
-
|
|
329
|
+
def analyze_markdown(md: str) -> list:
|
|
330
|
+
# ---------- for Readtime ----------
|
|
331
|
+
words = 0
|
|
332
|
+
cjk = 0
|
|
333
|
+
images = 0
|
|
334
|
+
table_rows = 0
|
|
335
|
+
code_rows = 0
|
|
336
|
+
math_blocks = 0
|
|
337
|
+
|
|
338
|
+
# ---------- for Summary ----------
|
|
339
|
+
summary_lines = []
|
|
276
340
|
|
|
277
341
|
state = "NORMAL"
|
|
278
342
|
fence = ""
|
|
@@ -281,7 +345,7 @@ def clean_markdown(md: str) -> list:
|
|
|
281
345
|
h1_parsed = False
|
|
282
346
|
math_delim = ""
|
|
283
347
|
|
|
284
|
-
for line in
|
|
348
|
+
for line in md.splitlines():
|
|
285
349
|
stripped = line.strip()
|
|
286
350
|
if not stripped:
|
|
287
351
|
continue
|
|
@@ -295,10 +359,12 @@ def clean_markdown(md: str) -> list:
|
|
|
295
359
|
state = "NORMAL"
|
|
296
360
|
frontmatter_parsed = True
|
|
297
361
|
continue
|
|
298
|
-
|
|
362
|
+
|
|
299
363
|
if state == "NORMAL" and stripped in ("---", "+++"):
|
|
300
364
|
state = "FRONTMATTER"
|
|
301
365
|
continue
|
|
366
|
+
else:
|
|
367
|
+
frontmatter_parsed = True
|
|
302
368
|
|
|
303
369
|
# ==================================================
|
|
304
370
|
# 2. Fence Block
|
|
@@ -306,6 +372,8 @@ def clean_markdown(md: str) -> list:
|
|
|
306
372
|
if state == "FENCE":
|
|
307
373
|
if stripped.startswith(fence):
|
|
308
374
|
state = "NORMAL"
|
|
375
|
+
else:
|
|
376
|
+
code_rows += 1
|
|
309
377
|
continue
|
|
310
378
|
|
|
311
379
|
if state == "NORMAL":
|
|
@@ -319,49 +387,51 @@ def clean_markdown(md: str) -> list:
|
|
|
319
387
|
# 3. HTML Comment
|
|
320
388
|
# ==================================================
|
|
321
389
|
if state == "COMMENT":
|
|
322
|
-
if
|
|
390
|
+
if stripped.endswith("-->"):
|
|
323
391
|
state = "NORMAL"
|
|
324
392
|
continue
|
|
325
393
|
|
|
326
|
-
if state == "NORMAL" and
|
|
394
|
+
if state == "NORMAL" and stripped.startswith("<!--"):
|
|
327
395
|
state = "COMMENT"
|
|
328
|
-
if
|
|
396
|
+
if stripped.endswith("-->"):
|
|
329
397
|
state = "NORMAL"
|
|
330
398
|
continue
|
|
331
399
|
|
|
332
400
|
# ==================================================
|
|
333
401
|
# 4. HTML Block
|
|
334
402
|
# ==================================================
|
|
403
|
+
# Counting img tags in html
|
|
404
|
+
lower = stripped.lower()
|
|
405
|
+
if "<img " in lower:
|
|
406
|
+
images += lower.count("<img ")
|
|
407
|
+
|
|
335
408
|
if state == "HTML_BLOCK":
|
|
336
|
-
if html_close_re and html_close_re
|
|
409
|
+
if html_close_re and html_close_re in lower:
|
|
337
410
|
state = "NORMAL"
|
|
338
411
|
html_close_re = None
|
|
339
412
|
continue
|
|
340
413
|
|
|
341
414
|
if state == "NORMAL":
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
415
|
+
if stripped.startswith("<"):
|
|
416
|
+
m = HTML_TAG_OPEN.match(stripped)
|
|
417
|
+
if m:
|
|
418
|
+
tag = m.group(1).lower()
|
|
419
|
+
|
|
420
|
+
# Normal tags: required </tag>
|
|
421
|
+
if tag in HTML_VALID_TAGS and tag not in HTML_VOID_TAGS:
|
|
422
|
+
html_close_re = f"</{tag}>"
|
|
423
|
+
if html_close_re in lower:
|
|
424
|
+
continue
|
|
425
|
+
else:
|
|
426
|
+
# VOID or Non-standard tags: as long as they end in >
|
|
427
|
+
html_close_re = ">"
|
|
428
|
+
if stripped.endswith(html_close_re):
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
# Going here means that the multiline HTML block
|
|
432
|
+
state = "HTML_BLOCK"
|
|
349
433
|
continue
|
|
350
434
|
|
|
351
|
-
# 非 void tag:进入 HTML_BLOCK
|
|
352
|
-
state = "HTML_BLOCK"
|
|
353
|
-
if is_void:
|
|
354
|
-
html_close_re = HTML_VOID_CLOSE_RE
|
|
355
|
-
else:
|
|
356
|
-
html_close_re = re.compile(HTML_TAG_CLOSE_TEMPLATE.format(re.escape(tag)), re.I)
|
|
357
|
-
|
|
358
|
-
# same-line close: <div>...</div>
|
|
359
|
-
if html_close_re.search(stripped):
|
|
360
|
-
state = "NORMAL"
|
|
361
|
-
html_close_re = None
|
|
362
|
-
|
|
363
|
-
continue
|
|
364
|
-
|
|
365
435
|
# ==================================================
|
|
366
436
|
# 5. Math Block
|
|
367
437
|
# ==================================================
|
|
@@ -373,29 +443,36 @@ def clean_markdown(md: str) -> list:
|
|
|
373
443
|
if state == "NORMAL" and stripped in ("$$", "\\["):
|
|
374
444
|
math_delim = "$$" if stripped == "$$" else "\\]"
|
|
375
445
|
state = "MATH"
|
|
446
|
+
math_blocks += 1
|
|
376
447
|
continue
|
|
377
448
|
|
|
378
449
|
# ==================================================
|
|
379
450
|
# 6. Inline Skip
|
|
380
451
|
# ==================================================
|
|
381
452
|
if state == "NORMAL":
|
|
382
|
-
if
|
|
453
|
+
if stripped.startswith("|") and stripped.endswith("|"):
|
|
454
|
+
# if TABLE_ROW_RE.match(stripped):
|
|
455
|
+
table_rows += 1
|
|
383
456
|
continue
|
|
384
|
-
if
|
|
457
|
+
if inline_skip(stripped):
|
|
385
458
|
continue
|
|
386
|
-
|
|
387
|
-
if SINGLE_LINE_HTML_NOISE.match(stripped):
|
|
459
|
+
if REF_LINK_RE.match(stripped):
|
|
388
460
|
continue
|
|
389
461
|
if not h1_parsed:
|
|
390
462
|
if H1_TITLE.match(stripped):
|
|
391
463
|
h1_parsed = True
|
|
392
464
|
continue
|
|
393
|
-
if stripped.startswith((
|
|
465
|
+
if stripped.startswith(("---", "***", "___")):
|
|
466
|
+
continue
|
|
467
|
+
if SINGLE_LINE_HTML_NOISE.match(stripped):
|
|
394
468
|
continue
|
|
395
469
|
|
|
396
470
|
# ==================================================
|
|
397
471
|
# 7. Inline Replace
|
|
398
472
|
# ==================================================
|
|
473
|
+
if "![" in stripped:
|
|
474
|
+
images += len(IMAGE_RE.findall(stripped))
|
|
475
|
+
|
|
399
476
|
text = stripped
|
|
400
477
|
text = IMAGE_RE.sub("", text)
|
|
401
478
|
text = LINK_RE.sub(r"\1", text)
|
|
@@ -403,19 +480,27 @@ def clean_markdown(md: str) -> list:
|
|
|
403
480
|
|
|
404
481
|
text = text.strip()
|
|
405
482
|
if text:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
483
|
+
cjk += len(CJK_RE.findall(text))
|
|
484
|
+
# CJK characters also match \w, so remove them before applying \w to avoid double counting!
|
|
485
|
+
text_no_cjk = CJK_RE.sub(" ", text)
|
|
486
|
+
words += len(WORD_RE.findall(text_no_cjk))
|
|
487
|
+
|
|
488
|
+
# Make the summary break early
|
|
489
|
+
if len(summary_lines) < 10:
|
|
490
|
+
summary_lines.append(text)
|
|
491
|
+
|
|
492
|
+
# ===============================
|
|
493
|
+
# compute read time
|
|
494
|
+
# ===============================
|
|
495
|
+
units = words + cjk / 2
|
|
496
|
+
seconds = math.ceil(units / DEFAULT_WPM * 60)
|
|
497
|
+
|
|
498
|
+
seconds += table_rows * 2
|
|
499
|
+
seconds += code_rows
|
|
500
|
+
seconds += math_blocks * 4
|
|
501
|
+
seconds += images * 2
|
|
502
|
+
|
|
503
|
+
summary = MD_SYNTAX_RE.sub("", " ".join(summary_lines)).strip()
|
|
504
|
+
minutes = max(1, math.ceil(seconds / 60))
|
|
505
|
+
|
|
506
|
+
return summary, minutes
|
{mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1/mkdocs_document_dates.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs-document-dates
|
|
3
|
-
Version: 3.7.
|
|
3
|
+
Version: 3.7.1
|
|
4
4
|
Summary: A new generation MkDocs plugin for displaying exact creation date, last updated date, authors, email of documents
|
|
5
5
|
Author-email: Aaron Wang <aaronwqt@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,7 +12,7 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.7
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: mkdocs
|
|
15
|
+
Requires-Dist: mkdocs<=1.6.1,>=1.6
|
|
16
16
|
Dynamic: license-file
|
|
17
17
|
|
|
18
18
|
# mkdocs-document-dates
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mkdocs<=1.6.1,>=1.6
|
|
@@ -4,13 +4,16 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mkdocs-document-dates"
|
|
7
|
-
version = "3.7.
|
|
7
|
+
version = "3.7.1"
|
|
8
8
|
description = "A new generation MkDocs plugin for displaying exact creation date, last updated date, authors, email of documents"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.7"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
authors = [{ name = "Aaron Wang", email = "aaronwqt@gmail.com" }]
|
|
13
|
-
dependencies = [
|
|
13
|
+
dependencies = [
|
|
14
|
+
"mkdocs>=1.6,<=1.6.1"
|
|
15
|
+
# "properdocs>=1.6.5"
|
|
16
|
+
]
|
|
14
17
|
classifiers = [
|
|
15
18
|
"Programming Language :: Python :: 3",
|
|
16
19
|
"Operating System :: OS Independent",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
mkdocs>=1.1.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/__init__.py
RENAMED
|
File without changes
|
{mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/hooks/pre-commit
RENAMED
|
File without changes
|
{mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.1}/mkdocs_document_dates/hooks_installer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|