mkdocs-document-dates 3.7.0__tar.gz → 3.7.2__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.
Files changed (44) hide show
  1. {mkdocs_document_dates-3.7.0/mkdocs_document_dates.egg-info → mkdocs_document_dates-3.7.2}/PKG-INFO +2 -2
  2. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/cache_manager.py +33 -13
  3. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/plugin.py +75 -72
  4. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/.DS_Store +0 -0
  5. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/templates/recently_updated_group.html +127 -35
  6. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/utils.py +169 -84
  7. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2/mkdocs_document_dates.egg-info}/PKG-INFO +2 -2
  8. mkdocs_document_dates-3.7.2/mkdocs_document_dates.egg-info/requires.txt +1 -0
  9. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/pyproject.toml +5 -2
  10. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/setup.py +1 -1
  11. mkdocs_document_dates-3.7.0/mkdocs_document_dates.egg-info/requires.txt +0 -1
  12. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/LICENSE +0 -0
  13. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/MANIFEST.in +0 -0
  14. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/README.md +0 -0
  15. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/__init__.py +0 -0
  16. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/hooks/pre-commit +0 -0
  17. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/hooks_installer.py +0 -0
  18. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/config/user.config.css +0 -0
  19. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/config/user.config.js +0 -0
  20. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/core/core.css +0 -0
  21. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/core/core.js +0 -0
  22. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/core/default.config.js +0 -0
  23. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/core/md5.min.js +0 -0
  24. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/core/timeago.full.min.js +0 -0
  25. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/core/timeago.min.js +0 -0
  26. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/core/utils.js +0 -0
  27. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/fonts/material-icons.css +0 -0
  28. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/fonts/materialicons.woff2 +0 -0
  29. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/templates/recently_updated_detail.html +0 -0
  30. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/templates/recently_updated_grid.html +0 -0
  31. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/templates/recently_updated_list.html +0 -0
  32. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/tippy/backdrop.css +0 -0
  33. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/tippy/light.css +0 -0
  34. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/tippy/material.css +0 -0
  35. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/tippy/popper.min.js +0 -0
  36. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/tippy/scale.css +0 -0
  37. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/tippy/shift-away.css +0 -0
  38. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/tippy/tippy.css +0 -0
  39. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates/static/tippy/tippy.umd.min.js +0 -0
  40. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates.egg-info/SOURCES.txt +0 -0
  41. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates.egg-info/dependency_links.txt +0 -0
  42. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates.egg-info/entry_points.txt +0 -0
  43. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/mkdocs_document_dates.egg-info/top_level.txt +0 -0
  44. {mkdocs_document_dates-3.7.0 → mkdocs_document_dates-3.7.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-document-dates
3
- Version: 3.7.0
3
+ Version: 3.7.2
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>=1.1.0
15
+ Requires-Dist: mkdocs<=1.6.1,>=1.6
16
16
  Dynamic: license-file
17
17
 
18
18
  # mkdocs-document-dates
@@ -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, get_file_creation_time, get_git_first_commit_time
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
- # 遍历 git_root 及子目录, 寻找 mkdocs.yml 文件
94
- for config_file in git_root.rglob('mkdocs.y*ml'):
95
- if config_file.name.lower() in ('mkdocs.yml', 'mkdocs.yaml'):
96
- projects.append(config_file.parent)
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 MkDocs projects: {e}")
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.warning(f"Document directory does not exist: {docs_dir}")
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 = get_file_creation_time(full_path).astimezone()
188
+ created_time = load_file_creation_date(full_path).astimezone()
169
189
  if not jsonl_cache_file.exists():
170
- git_time = get_git_first_commit_time(full_path)
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 Environment, FileSystemLoader, select_autoescape
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 get_file_creation_time, load_git_metadata, load_git_last_updated_date, read_jsonl_cache, compile_exclude_patterns, is_excluded, get_recently_updated_files
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.dates_cache = {}
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['plugins']['material/blog'].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.dates_cache = load_git_metadata(docs_dir_path)
70
- # 覆盖 jsonl 缓存
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.dates_cache:
76
- self.dates_cache[filename].update(new_info)
75
+ if filename in self.data_cached:
76
+ self.data_cached[filename].update(new_info)
77
77
 
78
- # 加载文档最近更新时间
79
- self.last_updated_dates = load_git_last_updated_date(docs_dir_path)
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._find_meta_date(page.meta, self.config['created_field_names'])
163
- updated = self._find_meta_date(page.meta, self.config['updated_field_names'])
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._get_file_creation_time(file_path, rel_path)
169
+ created = self._load_created_cached(file_path, rel_path)
166
170
  if not updated:
167
- updated = self._get_file_modification_time(file_path, rel_path)
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
- # 按 MaterialX 的数据规范给 meta 填充数据
173
- mx = page.meta.setdefault("_mx", {})
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' not in config:
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
- self.recent_docs_html = self._render_recently_updated_html(recently_updated_docs)
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
- default_template_path = Path(__file__).parent / 'static' / 'templates' / 'recently_updated_group.html'
262
- template_dir = default_template_path.parent
263
- template_file = default_template_path.name
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
- env = Environment(
267
- loader = FileSystemLoader(str(template_dir)),
268
- autoescape = select_autoescape(["html", "xml"])
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 _get_file_creation_time(self, file_path, rel_path):
295
+ def _load_created_cached(self, file_path, rel_path):
288
296
  # 优先从缓存中读取
289
- if rel_path in self.dates_cache:
290
- return datetime.fromisoformat(self.dates_cache[rel_path]['created'])
297
+ if rel_path in self.data_cached:
298
+ return datetime.fromisoformat(self.data_cached[rel_path]['created'])
291
299
  # 从文件系统获取
292
- return get_file_creation_time(file_path).astimezone()
300
+ return load_file_creation_date(file_path).astimezone()
293
301
 
294
- def _get_file_modification_time(self, file_path, rel_path):
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 _get_author_info(self, rel_path, page, config):
304
- # 1. meta author
305
- authors = self._process_meta_author(page.meta, page.url)
306
- if authors:
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._get_repaired_author(full_author, page.url))
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
- # 3. site_author 或 PC username
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._get_repaired_author(full_author, page.url)]
329
+ return [self._repair_author(full_author, page.url)]
327
330
  else:
328
331
  return [Author(name=name)]
329
332
 
330
- def _process_meta_author(self, meta, page_url):
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._get_repaired_author(full_author, page_url))
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._get_repaired_author(full_author, page_url)]
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 _get_repaired_author(self, author: Author, page_url: str) -> Author:
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 _get_formatted_date(self, date: datetime):
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._get_formatted_date(time_obj)}</time>"
405
+ f"<time datetime='{time_obj.isoformat()}'>{self._formatting_date(time_obj)}</time>"
403
406
  f"</span>"
404
407
  )
405
408
 
@@ -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
- /* --- 列表模式 (is-list) --- */
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: center;
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.84em;
108
- color: rgba(100, 100, 100, 0.4);
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,44 @@
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
- flex: 1;
130
- color: rgba(142, 142, 142, 1);
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
+ overflow: clip;
143
+ pointer-events: none;
144
+
145
+ color: var(--md-default-fg-color--light, rgb(121, 121, 121));
131
146
  font-size: 0.7rem;
132
- line-height: 1.6;
133
147
  letter-spacing: 0.02rem;
134
148
  word-break: break-all;
135
-
136
- /* 3 行文字截断,超出显示省略号 */
137
- display: -webkit-box;
138
- -webkit-box-orient: vertical;
139
- -webkit-line-clamp: 3;
140
- overflow: hidden;
149
+
150
+ -webkit-mask-image: linear-gradient(
151
+ to bottom,
152
+ black calc(100% - var(--lh)*1.2),
153
+ transparent 100%
154
+ );
155
+ mask-image: linear-gradient(
156
+ to bottom,
157
+ black calc(100% - var(--lh)*1.2),
158
+ transparent 100%
159
+ );
141
160
  }
142
161
  .card-cover {
143
- width: 120px;
144
- height: 80px;
145
- border-radius: 8px;
146
- flex-shrink: 0;
147
- display: flex;
148
- align-items: center;
149
- justify-content: center;
162
+ float: right;
163
+ width: 105px;
164
+ height: 70px;
165
+ margin: 2px 0 0 10px;
166
+ border-radius: 4px;
150
167
  overflow: hidden;
151
- margin-left: 10px;
168
+
152
169
  transition: filter 0.3s ease;
153
170
 
154
171
  /* Safari 圆角闪烁修复 */
@@ -181,6 +198,55 @@
181
198
  .card-cover:hover img {
182
199
  transform: scale(1.1) translateX(4px) translateZ(0);
183
200
  }
201
+ .card-body::after {
202
+ content: "";
203
+ display: block;
204
+ clear: both;
205
+ }
206
+
207
+ /* --- Footer --- */
208
+ .card-footer {
209
+ display: flex;
210
+ align-items: center;
211
+ margin-top: auto;
212
+ padding-top: 12px;
213
+ margin-bottom: -4px;
214
+ font-size: 0.75rem;
215
+ line-height: 1.6;
216
+ }
217
+ .card-footer .read-time {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ font-size: 0.65rem;
221
+ color: rgba(100, 100, 100, 0.6);
222
+ white-space: nowrap;
223
+ margin-right: 12px;
224
+ }
225
+ .card-footer .read-time-icon {
226
+ font-size: 0.8rem;
227
+ opacity: .7;
228
+ margin-right: 4px;
229
+ }
230
+
231
+ .md-typeset .md-tags:not([hidden]) {
232
+ margin: 0 0 0 auto;
233
+ display: flex;
234
+ align-items: center;
235
+ min-width: 0;
236
+ gap: 0;
237
+ flex-wrap: nowrap;
238
+ overflow: hidden;
239
+ }
240
+ .md-typeset .md-tag {
241
+ font-weight: 400;
242
+ flex-shrink: 0;
243
+ gap: 0;
244
+ color: var(--md-default-fg-color--light);
245
+ margin-right: 6px;
246
+ }
247
+ .md-typeset .md-tag-icon::before {
248
+ margin-right: 4px;
249
+ }
184
250
  </style>
185
251
 
186
252
  <div class="article-layout-switcher">
@@ -189,21 +255,47 @@
189
255
  <button class="layout-grid-btn"><span class="material-icons">view_module</span></button>
190
256
  </div>
191
257
 
192
- <div class="article-grid">
193
- {%- for mtime, rel_path, title, url, cover, summary in recent_docs %}
258
+ <div class="article-grid"
259
+ style="
260
+ --summary-lines-grid: {{ summary_lines.grid }};
261
+ --summary-lines-detail: {{ summary_lines.detail }};
262
+ ">
263
+ {%- for mtime, rel_path, title, url, cover, summary, readtime, tags in recent_docs %}
194
264
  <div class="article-card">
195
265
  <a class="card-header" href="{{ url }}" target="_blank">
196
266
  <div class="card-title">{{ title }}</div>
197
267
  <time class="dd-timeago card-date" datetime="{{ mtime }}">{{ mtime[:10] }}</time>
198
268
  </a>
199
269
  <div class="card-body">
200
- <div class="card-summary">{{ summary }}</div>
201
270
  {%- if cover %}
202
271
  <div class="card-cover">
203
272
  <img src="{{ cover }}" alt="{{ title }}">
204
273
  </div>
205
274
  {%- endif %}
275
+ <div class="card-summary">{{ summary }}</div>
276
+ </div>
277
+
278
+ {%- if tags or readtime > 0 %}
279
+ <div class="card-footer">
280
+ {%- if readtime > 0 %}
281
+ <span class="read-time"><span class="read-time-icon material-icons">hourglass_top</span>
282
+ {%- if HAS_LANGUAGE_TEMPLATE -%}
283
+ {%- import "partials/language.html" as lang with context -%}
284
+ {{ lang.t("readtime.other") | replace("#", readtime) }}
285
+ {%- else -%}
286
+ {{ readtime }} min read
287
+ {%- endif -%}
288
+ </span>
289
+ {%- endif %}
290
+ {%- if tags %}
291
+ <div class="md-tags">
292
+ {%- for tag in tags %}
293
+ <span class="md-tag md-tag-icon">{{- tag -}}</span>
294
+ {%- endfor %}
295
+ </div>
296
+ {%- endif %}
206
297
  </div>
298
+ {%- endif %}
207
299
  </div>
208
300
  {%- endfor %}
209
301
  </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 get_file_creation_time(file_path):
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 get file creation time for {file_path}: {e}")
52
+ logger.error(f"Failed to load file creation date for {file_path}: {e}")
52
53
  return datetime.now()
53
54
 
54
- def get_git_first_commit_time(file_path):
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 getting git first commit time for {file_path}: {e}")
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 load_git_last_updated_date(docs_dir_path: Path):
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
- # 获取文档标题和 URL
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._mx.document_dates.authors
175
+ # authors = file.page.meta.document_dates.authors
173
176
  if file.page.file:
174
- summary = extract_summary(file.page.file.content_string)
177
+ summary, readtime = analyze_markdown(file.page.file.content_string)
175
178
 
176
- # 存储信息
177
- files_meta.append((mtime, rel_path, title, url, cover, summary))
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'<\s*([a-zA-Z][\w\-]*)\b', re.I)
244
- HTML_TAG_CLOSE_TEMPLATE = r"</\s*{}\s*>"
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
- H1_TITLE = re.compile(r'^\s*# .+$', re.MULTILINE)
254
- SINGLE_LINE_HTML_NOISE = re.compile(r'^</?[a-z][\w-]*[^>]*>$', re.I)
255
- TABLE_ROW_RE = re.compile(r"^\s*\|.*\|\s*$")
256
- INLINE_SKIP_RE = re.compile(
257
- r"""
258
- ^\s*> | # quote
259
- ^\s*(?:!!!|\?\?\?) | # admonition
260
- ^\s*=== | # tab
261
- ^\s*\[.+?\]: # reference link, including footnote
262
- """,
263
- re.X,
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
- lines = md.splitlines()
275
- result = []
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 lines:
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 HTML_COMMENT_END.search(stripped):
390
+ if stripped.endswith("-->"):
323
391
  state = "NORMAL"
324
392
  continue
325
393
 
326
- if state == "NORMAL" and HTML_COMMENT_START.search(stripped):
394
+ if state == "NORMAL" and stripped.startswith("<!--"):
327
395
  state = "COMMENT"
328
- if HTML_COMMENT_END.search(stripped):
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.search(stripped):
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
- m = HTML_TAG_OPEN.match(stripped)
343
- if m:
344
- tag = m.group(1).lower()
345
- is_void = tag in HTML_VOID_TAGS
346
-
347
- # void tag:单行且以 > 结尾,视为直接结束,忽略该行
348
- if stripped.endswith('>') and is_void:
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 TABLE_ROW_RE.match(stripped):
453
+ if stripped.startswith("|") and stripped.endswith("|"):
454
+ # if TABLE_ROW_RE.match(stripped):
455
+ table_rows += 1
383
456
  continue
384
- if INLINE_SKIP_RE.match(stripped):
457
+ if inline_skip(stripped):
385
458
  continue
386
- # 单行 HTML 噪音兜底
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
- result.append(text)
407
-
408
- # 提前熔断
409
- if len(result) >= 10:
410
- break
411
-
412
- # 锁定 Frontmatter 状态,防止后续 --- 干扰
413
- frontmatter_parsed = True
414
-
415
- return result
416
- # return "\n".join(result)
417
-
418
- def extract_summary(markdown_text: str) -> str:
419
- md_list = clean_markdown(markdown_text)
420
- text = " ".join(md_list)
421
- return MD_SYNTAX_RE.sub('', text).strip()
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-document-dates
3
- Version: 3.7.0
3
+ Version: 3.7.2
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>=1.1.0
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.0"
7
+ version = "3.7.2"
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 = ["mkdocs>=1.1.0"]
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",
@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
4
4
  def legacy_setup():
5
5
  setup(
6
6
  name="mkdocs-document-dates",
7
- version="3.7.0",
7
+ version="3.7.2",
8
8
  author="Aaron Wang",
9
9
  author_email="aaronwqt@gmail.com",
10
10
  license="MIT",