mkdocs-document-dates 3.7.4__tar.gz → 3.8.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.
Files changed (43) hide show
  1. {mkdocs_document_dates-3.7.4/mkdocs_document_dates.egg-info → mkdocs_document_dates-3.8.1}/PKG-INFO +5 -3
  2. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/README.md +4 -2
  3. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/cache_manager.py +41 -30
  4. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/plugin.py +54 -66
  5. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/utils.py +79 -38
  6. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1/mkdocs_document_dates.egg-info}/PKG-INFO +5 -3
  7. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/pyproject.toml +1 -1
  8. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/setup.py +1 -1
  9. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/LICENSE +0 -0
  10. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/MANIFEST.in +0 -0
  11. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/__init__.py +0 -0
  12. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/hooks/pre-commit +0 -0
  13. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/hooks_installer.py +0 -0
  14. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/.DS_Store +0 -0
  15. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/config/user.config.css +0 -0
  16. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/config/user.config.js +0 -0
  17. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/core/core.css +0 -0
  18. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/core/core.js +0 -0
  19. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/core/default.config.js +0 -0
  20. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/core/md5.min.js +0 -0
  21. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/core/timeago.full.min.js +0 -0
  22. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/core/timeago.min.js +0 -0
  23. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/core/utils.js +0 -0
  24. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/fonts/material-icons.css +0 -0
  25. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/fonts/materialicons.woff2 +0 -0
  26. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/templates/recently_updated_detail.html +0 -0
  27. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/templates/recently_updated_grid.html +0 -0
  28. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/templates/recently_updated_group.html +0 -0
  29. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/templates/recently_updated_list.html +0 -0
  30. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/tippy/backdrop.css +0 -0
  31. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/tippy/light.css +0 -0
  32. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/tippy/material.css +0 -0
  33. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/tippy/popper.min.js +0 -0
  34. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/tippy/scale.css +0 -0
  35. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/tippy/shift-away.css +0 -0
  36. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/tippy/tippy.css +0 -0
  37. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates/static/tippy/tippy.umd.min.js +0 -0
  38. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates.egg-info/SOURCES.txt +0 -0
  39. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates.egg-info/dependency_links.txt +0 -0
  40. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates.egg-info/entry_points.txt +0 -0
  41. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates.egg-info/requires.txt +0 -0
  42. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/mkdocs_document_dates.egg-info/top_level.txt +0 -0
  43. {mkdocs_document_dates-3.7.4 → mkdocs_document_dates-3.8.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-document-dates
3
- Version: 3.7.4
3
+ Version: 3.8.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
@@ -44,6 +44,8 @@ A new generation MkDocs plugin for displaying exact **creation date, last update
44
44
  | git-revision-date-localized<br />git-authors | > 3 s | > 30 s | O(n) |
45
45
  | document-dates | < 0.1 s | < 0.15 s | O(1) |
46
46
 
47
+ > It's 20-500 times faster than `git-revision-date-localized` and `git-authors`
48
+
47
49
  ## Installation
48
50
 
49
51
  ```bash
@@ -82,8 +84,8 @@ In addition to the above basic configuration, the plug-in also provides a wealth
82
84
  - [Structure and Style](https://jaywhj.netlify.app/document-dates-en#Structure-and-Style): You can freely configure the plugin's display structure in mkdocs.yml or Front Matter. You can quickly set the plugin styles through preset entrances, such as icons, themes, colors, fonts, animations, dividing line and so on
83
85
  - [Template Variables](https://jaywhj.netlify.app/document-dates-en#Template-Variables): Can be used to optimize `sitemap.xml` for site SEO
84
86
  - [Recently Updated Module](https://jaywhj.netlify.app/document-dates-en#Recently-Updated-Module): Enable list of recently updated documents (in descending order of update date), this is ideal for sites with a large number of documents, so that readers can quickly see what's new
85
- - [Localization Language](https://jaywhj.netlify.app/document-dates-en#Localization-Language): More localization languages for `timeago` and `tooltip`
86
- - [Development Stories](https://jaywhj.netlify.app/document-dates-en#Development-Stories): Describes the origin of the plug-in, the difficulties and solutions encountered in development, and the principles and directions of product design
87
+ - [Localization Language](https://jaywhj.netlify.app/document-dates-en#Localization-Language): More localization languages for `timeago` and `tooltip`
88
+ - [Developer API](https://jaywhj.netlify.app/document-dates-en#Developer-API): Provides a date data API for developers, making it easy to retrieve exact dates in other plugins or hooks
87
89
 
88
90
  See the documentation for details: https://jaywhj.netlify.app/document-dates-en
89
91
 
@@ -26,6 +26,8 @@ A new generation MkDocs plugin for displaying exact **creation date, last update
26
26
  | git-revision-date-localized<br />git-authors | > 3 s | > 30 s | O(n) |
27
27
  | document-dates | < 0.1 s | < 0.15 s | O(1) |
28
28
 
29
+ > It's 20-500 times faster than `git-revision-date-localized` and `git-authors`
30
+
29
31
  ## Installation
30
32
 
31
33
  ```bash
@@ -64,8 +66,8 @@ In addition to the above basic configuration, the plug-in also provides a wealth
64
66
  - [Structure and Style](https://jaywhj.netlify.app/document-dates-en#Structure-and-Style): You can freely configure the plugin's display structure in mkdocs.yml or Front Matter. You can quickly set the plugin styles through preset entrances, such as icons, themes, colors, fonts, animations, dividing line and so on
65
67
  - [Template Variables](https://jaywhj.netlify.app/document-dates-en#Template-Variables): Can be used to optimize `sitemap.xml` for site SEO
66
68
  - [Recently Updated Module](https://jaywhj.netlify.app/document-dates-en#Recently-Updated-Module): Enable list of recently updated documents (in descending order of update date), this is ideal for sites with a large number of documents, so that readers can quickly see what's new
67
- - [Localization Language](https://jaywhj.netlify.app/document-dates-en#Localization-Language): More localization languages for `timeago` and `tooltip`
68
- - [Development Stories](https://jaywhj.netlify.app/document-dates-en#Development-Stories): Describes the origin of the plug-in, the difficulties and solutions encountered in development, and the principles and directions of product design
69
+ - [Localization Language](https://jaywhj.netlify.app/document-dates-en#Localization-Language): More localization languages for `timeago` and `tooltip`
70
+ - [Developer API](https://jaywhj.netlify.app/document-dates-en#Developer-API): Provides a date data API for developers, making it easy to retrieve exact dates in other plugins or hooks
69
71
 
70
72
  See the documentation for details: https://jaywhj.netlify.app/document-dates-en
71
73
 
@@ -10,13 +10,19 @@ from .utils import read_jsonl_cache, write_jsonl_cache, load_file_creation_date,
10
10
  logger = logging.getLogger("mkdocs.plugins.document_dates")
11
11
  _LOGGING_CONFIGURED = False
12
12
 
13
+ CONFIG_PRIORITY = {
14
+ "mkdocs.yml": 0,
15
+ "properdocs.yml": 1,
16
+ "mkdocs.yaml": 2,
17
+ "properdocs.yaml": 3,
18
+ }
13
19
 
14
20
  def _default_log_file() -> Path:
15
21
  try:
16
22
  git_root = Path(subprocess.check_output(
17
- ['git', 'rev-parse', '--show-toplevel'],
23
+ ["git", "rev-parse", "--show-toplevel"],
18
24
  env=_clean_git_env(),
19
- encoding='utf-8'
25
+ encoding="utf-8"
20
26
  ).strip())
21
27
  base_dir = git_root
22
28
  except Exception:
@@ -82,20 +88,29 @@ def _clean_git_env():
82
88
 
83
89
  return env
84
90
 
85
- def find_mkdocs_projects():
86
- projects = set()
91
+ def find_mkdocs_projects() -> dict[Path, Path]:
92
+ projects = {}
87
93
 
88
94
  try:
89
95
  git_root = Path(subprocess.check_output(
90
- ['git', 'rev-parse', '--show-toplevel'],
96
+ ["git", "rev-parse", "--show-toplevel"],
91
97
  env=_clean_git_env(),
92
- encoding='utf-8'
98
+ encoding="utf-8"
93
99
  ).strip())
94
100
 
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)
101
+ for pattern in ("*.yml", "*.yaml"):
102
+ for config_file in git_root.rglob(pattern):
103
+ name = config_file.name.lower()
104
+ if name not in CONFIG_PRIORITY:
105
+ continue
106
+
107
+ project_dir = config_file.parent
108
+ existing = projects.get(project_dir)
109
+ if existing is None:
110
+ projects[project_dir] = config_file
111
+ continue
112
+ if CONFIG_PRIORITY[name] < CONFIG_PRIORITY[existing.name.lower()]:
113
+ projects[project_dir] = config_file
99
114
 
100
115
  if not projects:
101
116
  logger.warning("No MkDocs/ProperDocs projects found in the repository")
@@ -105,19 +120,19 @@ def find_mkdocs_projects():
105
120
  except Exception as e:
106
121
  logger.error(f"Unexpected error while searching for projects: {e}")
107
122
 
108
- return list(projects)
123
+ return projects
109
124
 
110
125
  def setup_gitattributes(docs_dir: Path):
111
126
  try:
112
- gitattributes_path = docs_dir / '.gitattributes'
127
+ gitattributes_path = docs_dir / ".gitattributes"
113
128
  union_merge_line = ".dates_cache.jsonl merge=union"
114
129
  # custom_merge_line = ".dates_cache.json merge=custom_json_merge"
115
- content = gitattributes_path.read_text(encoding='utf-8') if gitattributes_path.exists() else ""
130
+ content = gitattributes_path.read_text(encoding="utf-8") if gitattributes_path.exists() else ""
116
131
  if union_merge_line not in content:
117
- if content and not content.endswith('\n'):
118
- content += '\n'
132
+ if content and not content.endswith("\n"):
133
+ content += "\n"
119
134
  content += f"{union_merge_line}\n"
120
- gitattributes_path.write_text(content, encoding='utf-8')
135
+ gitattributes_path.write_text(content, encoding="utf-8")
121
136
  subprocess.run(["git", "add", str(gitattributes_path)], cwd=docs_dir, env=_clean_git_env(), check=True)
122
137
  logger.info(f"Updated .gitattributes file: {gitattributes_path}")
123
138
  return True
@@ -134,18 +149,14 @@ def update_cache():
134
149
  configure_file_logging(_default_log_file())
135
150
 
136
151
  global_updated = False
137
- for project_dir in find_mkdocs_projects():
152
+ for project_dir, mkdocs_yml in find_mkdocs_projects().items():
138
153
  try:
139
154
  project_updated = False
140
155
 
141
- docs_dir = project_dir / 'docs'
156
+ docs_dir = project_dir / "docs"
142
157
 
143
158
  # 从 mkdocs.yml 中读取 docs_dir 配置覆盖默认值
144
159
  try:
145
- mkdocs_yml = project_dir / "properdocs.yml"
146
- if not mkdocs_yml.exists():
147
- mkdocs_yml = project_dir / "mkdocs.yml"
148
-
149
160
  mkdocs_config = yaml.load(
150
161
  mkdocs_yml.read_text(encoding="utf-8"),
151
162
  Loader=yaml.BaseLoader,
@@ -156,16 +167,16 @@ def update_cache():
156
167
  except (IOError, OSError, yaml.YAMLError) as e:
157
168
  logger.warning(f"Failed to read docs_dir: {e}")
158
169
 
159
- if not docs_dir.exists():
170
+ if not docs_dir.is_dir():
160
171
  logger.info(f"Document directory does not exist: {docs_dir}")
161
172
  continue
162
173
 
163
174
  # 设置.gitattributes文件
164
- global_updated = setup_gitattributes(docs_dir)
175
+ global_updated |= setup_gitattributes(docs_dir)
165
176
 
166
177
  # 获取docs目录下已跟踪(tracked)的markdown文件
167
- cmd = ["git", "ls-files", "*.md"]
168
- result = subprocess.run(cmd, cwd=docs_dir, env=_clean_git_env(), capture_output=True, encoding='utf-8')
178
+ cmd = ["git", "-c", "core.quotepath=false", "ls-files", "*.md"]
179
+ result = subprocess.run(cmd, cwd=docs_dir, env=_clean_git_env(), capture_output=True, encoding="utf-8")
169
180
  tracked_files = result.stdout.splitlines() if result.stdout else []
170
181
 
171
182
  if not tracked_files:
@@ -173,7 +184,7 @@ def update_cache():
173
184
  continue
174
185
 
175
186
  # 读取 JSONL 缓存
176
- jsonl_cache_file = docs_dir / '.dates_cache.jsonl'
187
+ jsonl_cache_file = docs_dir / ".dates_cache.jsonl"
177
188
  jsonl_dates_cache = read_jsonl_cache(jsonl_cache_file)
178
189
 
179
190
  # 根据 git已跟踪的文件来更新
@@ -185,13 +196,13 @@ def update_cache():
185
196
 
186
197
  full_path = docs_dir / rel_path
187
198
  if full_path.exists():
188
- created_time = load_file_creation_date(full_path).astimezone()
199
+ created_time = load_file_creation_date(full_path)
189
200
  if not jsonl_cache_file.exists():
190
201
  git_time = load_git_first_commit_date(full_path)
191
202
  if git_time:
192
203
  created_time = min(created_time, git_time)
193
204
  jsonl_dates_cache[rel_path] = {
194
- "created": created_time.isoformat()
205
+ "created": int(created_time.timestamp())
195
206
  }
196
207
  project_updated = True
197
208
  except Exception as e:
@@ -204,7 +215,7 @@ def update_cache():
204
215
 
205
216
  # 如果有更新,写入 JSONL 缓存文件
206
217
  if project_updated or not jsonl_cache_file.exists():
207
- global_updated = write_jsonl_cache(jsonl_cache_file, jsonl_dates_cache, tracked_files)
218
+ global_updated |= write_jsonl_cache(jsonl_cache_file, jsonl_dates_cache, tracked_files)
208
219
  except subprocess.CalledProcessError as e:
209
220
  logger.error(f"Failed to execute git command: {e}")
210
221
  continue
@@ -1,16 +1,15 @@
1
- import os
2
1
  import yaml
3
2
  import shutil
4
3
  import logging
5
4
  from jinja2 import ChoiceLoader, FileSystemLoader
6
- from datetime import datetime
5
+ from datetime import datetime, timezone
7
6
  from pathlib import Path
8
7
  from mkdocs.plugins import BasePlugin, event_priority
9
8
  from mkdocs.config import config_options
10
9
  from mkdocs.structure.pages import Page
11
10
  from mkdocs.utils import get_relative_url
12
11
  from urllib.parse import urlparse
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
12
+ from .utils import compile_exclude_patterns, is_excluded, get_recently_updated_files, load_dates_and_authors
14
13
 
15
14
  logger = logging.getLogger("mkdocs.plugins.document_dates")
16
15
  logger.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, CRITICAL
@@ -45,18 +44,23 @@ class DocumentDatesPlugin(BasePlugin):
45
44
  super().__init__()
46
45
 
47
46
  self.data_cached = {}
48
- self.last_updated_dates = {}
49
47
  self.authors_yml = {}
50
48
  self.recent_docs_html = None
51
49
  self.recent_enable = False
52
50
  self._exclude_patterns = []
53
51
 
54
52
  def on_config(self, config):
55
- docs_dir_path = Path(config['docs_dir'])
53
+ docs_dir_path = Path(config.docs_dir)
56
54
 
57
55
  # 加载 author 配置
58
- authors_file = docs_dir_path / 'authors.yml'
59
- if not authors_file.exists():
56
+ authors_file = None
57
+ for name in ("authors.yml", "authors.yaml"):
58
+ candidate = docs_dir_path / name
59
+ if candidate.exists():
60
+ authors_file = candidate
61
+ break
62
+
63
+ if authors_file is None:
60
64
  try:
61
65
  blog_config = config.plugins.get(f"{config.theme.name}/blog").config
62
66
  authors_file_resolved = blog_config.authors_file.format(blog=blog_config.blog_dir)
@@ -65,20 +69,6 @@ class DocumentDatesPlugin(BasePlugin):
65
69
  pass
66
70
  self._load_authors_from_yaml(authors_file)
67
71
 
68
- # 加载文档 git 元数据(日期 & 作者)
69
- self.data_cached = load_git_metadata(docs_dir_path)
70
- # 加载 jsonl 缓存数据
71
- jsonl_cache_file = docs_dir_path / '.dates_cache.jsonl'
72
- if jsonl_cache_file.exists():
73
- jsonl_cache = read_jsonl_cache(jsonl_cache_file)
74
- for filename, new_info in jsonl_cache.items():
75
- if filename in self.data_cached:
76
- self.data_cached[filename].update(new_info)
77
-
78
- # 加载文档最近更新时间(日期)
79
- self.last_updated_dates = load_git_last_updated_dates(docs_dir_path)
80
-
81
-
82
72
  # 复制配置文件到用户目录(如果不存在)
83
73
  dest_dir = docs_dir_path / 'assets' / 'document_dates'
84
74
  dest_dir.mkdir(parents=True, exist_ok=True)
@@ -151,32 +141,41 @@ class DocumentDatesPlugin(BasePlugin):
151
141
 
152
142
  return config
153
143
 
144
+ @event_priority(50)
145
+ def on_files(self, files, config):
146
+ self.data_cached = load_dates_and_authors(Path(config.docs_dir), files)
147
+ return files
148
+
154
149
  @event_priority(50)
155
150
  def on_page_markdown(self, markdown, page: Page, config, files):
156
151
  # 获取相对路径,src_uri 总是以"/"分隔
157
- rel_path = getattr(page.file, 'src_uri', page.file.src_path)
158
- if os.sep != '/':
159
- rel_path = rel_path.replace(os.sep, '/')
160
- file_path = page.file.abs_src_path
161
-
152
+ rel_path = getattr(page.file, 'src_uri')
153
+
162
154
  # 优先获取 page.meta 中的数据
163
155
  created = self._load_meta_date(page.meta, self.config['created_field_names'])
164
156
  updated = self._load_meta_date(page.meta, self.config['updated_field_names'])
165
157
  authors = self._load_meta_author(page.meta, page.url)
166
158
 
167
- # 再获取缓存的数据
168
- if not created:
169
- created = self._load_created_cached(file_path, rel_path)
170
- if not updated:
171
- updated = self._load_updated_cached(file_path, rel_path)
159
+ # 如果 meta 数据存在,则存储
160
+ cache = self.data_cached.setdefault(rel_path, {})
161
+ if created:
162
+ cache['created'] = created
163
+ else:
164
+ created = cache.get('created')
165
+
166
+ if updated:
167
+ cache['updated'] = updated
168
+ else:
169
+ updated = cache.get('updated')
170
+
172
171
  if not authors:
173
172
  authors = self._load_author_cached(rel_path, page, config)
174
173
 
175
- # 注入数据
174
+ # 注入数据到模板 (utc datetime -> local datetime)
176
175
  page.meta["document_dates"] = {
177
176
  "dates": {
178
- "created": created.isoformat(),
179
- "updated": updated.isoformat(),
177
+ "created": created.astimezone().isoformat() if created else None,
178
+ "updated": updated.astimezone().isoformat() if updated else None,
180
179
  },
181
180
  "authors": authors
182
181
  }
@@ -206,13 +205,13 @@ class DocumentDatesPlugin(BasePlugin):
206
205
  limit = recently_updated_config.get('limit', 10)
207
206
 
208
207
  # 获取站点 URL 路径前缀
209
- site_url = config.get("site_url", "")
208
+ site_url = config.get("site_url") or ""
210
209
  base_path = urlparse(site_url).path.rstrip("/")
211
210
  prefix = f"{base_path}/" if base_path else "/"
212
211
 
213
212
  # 获取最近更新的文档数据
214
213
  recent_exclude_patterns = compile_exclude_patterns(exclude_list)
215
- recently_updated_docs = get_recently_updated_files(self.last_updated_dates, files, recent_exclude_patterns, limit, self.recent_enable, prefix)
214
+ recently_updated_docs = get_recently_updated_files(self.data_cached, files, recent_exclude_patterns, limit, self.recent_enable, prefix)
216
215
 
217
216
  # 将数据注入到 config['extra'] 中供全局访问
218
217
  if not config.get('extra', {}).get("recently_updated_docs", {}):
@@ -290,42 +289,31 @@ class DocumentDatesPlugin(BasePlugin):
290
289
  for field in field_names:
291
290
  if field in meta:
292
291
  try:
293
- # 移除首尾可能存在的单双引号和时区信息
292
+ # 移除首尾可能存在的单双引号
294
293
  date_str = str(meta[field]).strip("'\"")
295
- return datetime.fromisoformat(date_str).replace(tzinfo=None)
294
+ dt = datetime.fromisoformat(date_str)
295
+ # 如果没时区,则当成本地时间,再转 UTC
296
+ if dt.tzinfo is None:
297
+ local_tz = datetime.now().astimezone().tzinfo
298
+ dt = dt.replace(tzinfo=local_tz)
299
+ return dt.astimezone(timezone.utc)
300
+ # return datetime.fromisoformat(date_str).astimezone()
296
301
  except Exception:
297
302
  continue
298
303
  return None
299
304
 
300
- def _load_created_cached(self, file_path, rel_path):
301
- # 优先从缓存中读取
302
- if rel_path in self.data_cached:
303
- return datetime.fromisoformat(self.data_cached[rel_path]['created'])
304
- # 从文件系统获取
305
- return load_file_creation_date(file_path).astimezone()
306
-
307
- def _load_updated_cached(self, file_path, rel_path):
308
- # 优先从缓存中读取
309
- if rel_path in self.last_updated_dates:
310
- return datetime.fromtimestamp(self.last_updated_dates[rel_path]).astimezone()
311
- # 从文件系统获取最后修改时间
312
- stat = os.stat(file_path)
313
- return datetime.fromtimestamp(stat.st_mtime).astimezone()
314
-
315
-
316
305
  def _load_author_cached(self, rel_path, page, config):
317
306
  # 1. git author
318
- if rel_path in self.data_cached:
319
- authors_list = self.data_cached[rel_path].get('authors')
320
- if authors_list:
321
- authors = []
322
- for data in authors_list:
323
- full_author = self.authors_yml.get(data['name'])
324
- if full_author:
325
- authors.append(self._repair_author(full_author, page.url))
326
- else:
327
- authors.append(Author(**data))
328
- return authors
307
+ authors_list = self.data_cached.get(rel_path, {}).get('authors', None)
308
+ if authors_list:
309
+ authors = []
310
+ for data in authors_list:
311
+ full_author = self.authors_yml.get(data['name'])
312
+ if full_author:
313
+ authors.append(self._repair_author(full_author, page.url))
314
+ else:
315
+ authors.append(Author(**data))
316
+ return authors
329
317
 
330
318
  # 2. site_author 或 PC username
331
319
  name = config.get('site_author') or Path.home().name
@@ -407,7 +395,7 @@ class DocumentDatesPlugin(BasePlugin):
407
395
  return (
408
396
  f"<span class='dd-item' data-tippy-content data-tippy-raw='{formatted}'>"
409
397
  f"<span class='material-icons' data-icon='{icon}'></span>"
410
- f"<time datetime='{time_obj.isoformat()}'>{self._formatting_date(time_obj)}</time>"
398
+ f"<time datetime='{time_obj.astimezone().isoformat()}'>{self._formatting_date(time_obj)}</time>"
411
399
  f"</span>"
412
400
  )
413
401
 
@@ -9,7 +9,7 @@ import re
9
9
  import math
10
10
  from pathlib import Path
11
11
  from operator import itemgetter
12
- from datetime import datetime
12
+ from datetime import datetime, timezone
13
13
  from collections import defaultdict
14
14
  from mkdocs.structure.files import Files
15
15
 
@@ -17,6 +17,44 @@ logger = logging.getLogger("mkdocs.plugins.document_dates")
17
17
  logger.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, CRITICAL
18
18
 
19
19
 
20
+ def load_dates_and_authors(docs_dir_path: Path, files: Files):
21
+
22
+ # git 创建日期
23
+ created_data = load_git_metadata(docs_dir_path)
24
+
25
+ # 覆盖 jsonl 创建日期
26
+ jsonl_cache_file = docs_dir_path / '.dates_cache.jsonl'
27
+ if jsonl_cache_file.exists():
28
+ jsonl_cache = read_jsonl_cache(jsonl_cache_file)
29
+ for filename, new_info in jsonl_cache.items():
30
+ if filename in created_data:
31
+ created_data[filename].update(new_info)
32
+
33
+ # git 更新日期
34
+ updated_data = load_git_last_updated_dates(docs_dir_path)
35
+
36
+ for file in files:
37
+ if file.inclusion.is_excluded():
38
+ continue
39
+ if not file.src_path.endswith('.md'):
40
+ continue
41
+ rel_path = getattr(file, 'src_uri')
42
+
43
+ # created: timestamp -> datetime
44
+ cache = created_data.setdefault(rel_path, {})
45
+ created_ts = cache.get('created')
46
+ cache['created'] = (
47
+ datetime.fromtimestamp(created_ts, tz=timezone.utc)
48
+ if created_ts is not None
49
+ else load_file_creation_date(file.abs_src_path)
50
+ )
51
+
52
+ # updated: timestamp -> datetime
53
+ mtime = updated_data.get(rel_path, os.path.getmtime(file.abs_src_path))
54
+ created_data[rel_path]['updated'] = datetime.fromtimestamp(mtime, tz=timezone.utc)
55
+
56
+ return created_data
57
+
20
58
  def compile_exclude_patterns(exclude_list):
21
59
  if not exclude_list:
22
60
  return []
@@ -36,34 +74,34 @@ def is_excluded(path, patterns):
36
74
  return True
37
75
  return False
38
76
 
39
- def load_file_creation_date(file_path):
77
+ def load_file_creation_date(file_path) -> datetime:
40
78
  try:
41
79
  stat = os.stat(file_path)
42
80
  system = platform.system().lower()
43
81
  if system.startswith('win'): # Windows
44
- return datetime.fromtimestamp(stat.st_ctime)
82
+ return datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc)
45
83
  elif system == 'darwin': # macOS
46
84
  try:
47
- return datetime.fromtimestamp(stat.st_birthtime)
85
+ return datetime.fromtimestamp(stat.st_birthtime, tz=timezone.utc)
48
86
  except AttributeError:
49
- return datetime.fromtimestamp(stat.st_ctime)
87
+ return datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc)
50
88
  else: # Linux, 没有创建时间,使用修改时间
51
- return datetime.fromtimestamp(stat.st_mtime)
89
+ return datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
52
90
  except (OSError, ValueError) as e:
53
91
  logger.error(f"Failed to load file creation date for {file_path}: {e}")
54
- return datetime.now()
92
+ return datetime.now(timezone.utc)
55
93
 
56
- def load_git_first_commit_date(file_path):
94
+ def load_git_first_commit_date(file_path) -> datetime:
57
95
  try:
58
96
  # git log --reverse --format="%aI" -- {file_path} | head -n 1
59
- cmd_list = ['git', 'log', '--reverse', '--format=%aI', '--', file_path]
97
+ cmd_list = ['git', 'log', '--reverse', '--format=%at', '--', file_path]
60
98
  process = subprocess.run(cmd_list, capture_output=True, encoding='utf-8')
61
99
  if process.returncode == 0 and process.stdout.strip():
62
- first_line = process.stdout.partition('\n')[0].strip()
63
- return datetime.fromisoformat(first_line)
100
+ first_line = int(process.stdout.partition('\n')[0].strip())
101
+ return datetime.fromtimestamp(first_line, tz=timezone.utc)
64
102
  except Exception as e:
65
103
  logger.info(f"Error load git first commit date for {file_path}: {e}")
66
- return None
104
+ return datetime.now(timezone.utc)
67
105
 
68
106
  def load_git_metadata(docs_dir_path: Path):
69
107
  dates_cache = {}
@@ -74,7 +112,7 @@ def load_git_metadata(docs_dir_path: Path):
74
112
  ).strip())
75
113
  rel_docs_path = docs_dir_path.relative_to(git_root).as_posix()
76
114
 
77
- cmd = ['git', 'log', '--reverse', '--no-merges', '--use-mailmap', '--name-only', '--format=%aN|%aE|%aI', f'--relative={rel_docs_path}', '--', '*.md']
115
+ cmd = ['git', '-c', 'core.quotepath=false', 'log', '--reverse', '--no-merges', '--use-mailmap', '--name-only', '--format=%aN|%aE|%at', f'--relative={rel_docs_path}', '--', '*.md']
78
116
  process = subprocess.run(cmd, cwd=docs_dir_path, capture_output=True, encoding='utf-8')
79
117
  if process.returncode == 0:
80
118
  authors_dict = defaultdict(dict)
@@ -93,7 +131,7 @@ def load_git_metadata(docs_dir_path: Path):
93
131
  # a.巧用 Python 字典的 setdefault 特性来去重(setdefault 为不存在的键提供初始值,不会覆盖已有值)
94
132
  # b.巧用 Python 字典的插入顺序特性来保留内容插入顺序(Python 3.7+ 字典会保持插入顺序)
95
133
  authors_dict[line].setdefault((name, email), None)
96
- first_commit.setdefault(line, created)
134
+ first_commit.setdefault(line, int(created))
97
135
 
98
136
  # 构建最终的缓存数据
99
137
  for file_path in first_commit:
@@ -118,11 +156,11 @@ def load_git_last_updated_dates(docs_dir_path: Path):
118
156
  ).strip())
119
157
  rel_docs_path = docs_dir_path.relative_to(git_root).as_posix()
120
158
 
121
- cmd = ['git', 'log', '--no-merges', '--use-mailmap', '--format=%aN|%aE|%at', '--name-only', f'--relative={rel_docs_path}', '--', '*.md']
159
+ cmd = ['git', '-c', 'core.quotepath=false', 'log', '--no-merges', '--use-mailmap', '--format=%aN|%aE|%at', '--name-only', f'--relative={rel_docs_path}', '--', '*.md']
122
160
  process = subprocess.run(cmd, cwd=docs_dir_path, capture_output=True, encoding='utf-8')
123
161
  if process.returncode == 0:
124
162
  result = subprocess.run(
125
- ["git", "ls-files", "*.md"],
163
+ ['git', '-c', 'core.quotepath=false', 'ls-files', '*.md'],
126
164
  cwd=docs_dir_path, capture_output=True, encoding='utf-8'
127
165
  )
128
166
  # 只记录已跟踪的文件(还有已删除、重命名、不再跟踪)
@@ -134,7 +172,7 @@ def load_git_last_updated_dates(docs_dir_path: Path):
134
172
  if not line:
135
173
  continue
136
174
  if '|' in line:
137
- ts = float(line.split('|')[2])
175
+ ts = int(line.split('|')[2])
138
176
  elif line.endswith('.md') and line in tracked_files and ts:
139
177
  # 只记录第一次出现的文件,即最近一次提交(setdefault 机制不会覆盖已有值)
140
178
  doc_mtime_map.setdefault(line, ts)
@@ -153,14 +191,13 @@ def get_recently_updated_files(existing_dates: dict, files: Files, exclude_list:
153
191
  continue
154
192
  if not file.src_path.endswith('.md'):
155
193
  continue
156
- rel_path = getattr(file, 'src_uri', file.src_path)
157
- if os.sep != '/':
158
- rel_path = rel_path.replace(os.sep, '/')
194
+ rel_path = getattr(file, 'src_uri')
159
195
  if is_excluded(rel_path, exclude_list):
160
196
  continue
161
197
 
162
198
  # 优先从现有数据获取 mtime,如果不存在则 fallback 到文件系统 mtime
163
- mtime = existing_dates.get(rel_path, os.path.getmtime(file.abs_src_path))
199
+ exist_updated: datetime = existing_dates.get(rel_path, {}).get('updated')
200
+ mtime = exist_updated.timestamp() if exist_updated else os.path.getmtime(file.abs_src_path)
164
201
 
165
202
  # 获取文档其它信息
166
203
  title = file.page.title if file.page and file.page.title else file.name
@@ -193,23 +230,17 @@ def get_recently_updated_files(existing_dates: dict, files: Files, exclude_list:
193
230
  "readtime": readtime,
194
231
  "tags": tags,
195
232
  })
196
- # existing_map[rel_path] = mtime
197
233
 
198
234
  # 构建最近更新列表
199
235
  if files_meta:
200
- fmt_full = "%Y-%m-%d %H:%M:%S"
201
- fmt_date = "%Y-%m-%d"
202
236
  # heapq 取 top limit
203
- top_results = heapq.nlargest(limit, files_meta, key=itemgetter("updated_ts"))
204
-
205
- for doc in top_results:
206
- dt = datetime.fromtimestamp(doc["updated_ts"])
237
+ recently_updated_results = heapq.nlargest(limit, files_meta, key=itemgetter("updated_ts"))
207
238
 
208
- recently_updated_results.append({
209
- **doc,
210
- "updated_dt": dt.strftime(fmt_full),
211
- "updated": dt.strftime(fmt_date),
212
- })
239
+ for doc in recently_updated_results:
240
+ # timestamp -> utc datetime -> local datetime
241
+ dt = datetime.fromtimestamp(doc["updated_ts"], tz=timezone.utc).astimezone()
242
+ doc["updated_dt"] = dt.isoformat()
243
+ doc["updated"] = dt.date().isoformat()
213
244
 
214
245
  return recently_updated_results
215
246
 
@@ -223,10 +254,16 @@ def read_jsonl_cache(jsonl_file: Path):
223
254
  entry = json.loads(line.strip())
224
255
  if entry and isinstance(entry, dict) and len(entry) == 1:
225
256
  file_path, file_info = next(iter(entry.items()))
226
- dates_cache[file_path] = file_info
227
- except (json.JSONDecodeError, StopIteration) as e:
257
+ if isinstance(file_info, dict):
258
+ created = file_info.get('created')
259
+ if isinstance(created, str):
260
+ file_info['created'] = int(datetime.fromisoformat(created).timestamp())
261
+ elif isinstance(created, (int, float)):
262
+ file_info['created'] = int(created)
263
+ dates_cache[file_path] = file_info
264
+ except (json.JSONDecodeError, StopIteration, ValueError, TypeError,) as e:
228
265
  logger.warning(f"Skipping invalid JSONL line: {e}")
229
- except IOError as e:
266
+ except OSError as e:
230
267
  logger.warning(f"Error reading from '.dates_cache.jsonl': {str(e)}")
231
268
  return dates_cache
232
269
 
@@ -237,7 +274,11 @@ def write_jsonl_cache(jsonl_file: Path, dates_cache, tracked_files):
237
274
  with open(temp_file, 'w', encoding='utf-8') as f:
238
275
  for file_path in tracked_files:
239
276
  if file_path in dates_cache:
240
- entry = {file_path: dates_cache[file_path]}
277
+ file_info = dates_cache[file_path].copy()
278
+ created = file_info.get('created')
279
+ if created is not None:
280
+ file_info['created'] = datetime.fromtimestamp(created, tz=timezone.utc).isoformat()
281
+ entry = {file_path: file_info}
241
282
  f.write(json.dumps(entry, ensure_ascii=False) + '\n')
242
283
 
243
284
  # 替换原文件
@@ -247,7 +288,7 @@ def write_jsonl_cache(jsonl_file: Path, dates_cache, tracked_files):
247
288
  subprocess.run(["git", "add", str(jsonl_file)], check=True)
248
289
  logger.info(f"Successfully updated JSONL cache file: {jsonl_file}")
249
290
  return True
250
- except (IOError, json.JSONDecodeError) as e:
291
+ except OSError as e:
251
292
  logger.warning(f"Failed to write JSONL cache file {jsonl_file}: {e}")
252
293
  except Exception as e:
253
294
  logger.warning(f"Failed to add JSONL cache file to git: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-document-dates
3
- Version: 3.7.4
3
+ Version: 3.8.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
@@ -44,6 +44,8 @@ A new generation MkDocs plugin for displaying exact **creation date, last update
44
44
  | git-revision-date-localized<br />git-authors | > 3 s | > 30 s | O(n) |
45
45
  | document-dates | < 0.1 s | < 0.15 s | O(1) |
46
46
 
47
+ > It's 20-500 times faster than `git-revision-date-localized` and `git-authors`
48
+
47
49
  ## Installation
48
50
 
49
51
  ```bash
@@ -82,8 +84,8 @@ In addition to the above basic configuration, the plug-in also provides a wealth
82
84
  - [Structure and Style](https://jaywhj.netlify.app/document-dates-en#Structure-and-Style): You can freely configure the plugin's display structure in mkdocs.yml or Front Matter. You can quickly set the plugin styles through preset entrances, such as icons, themes, colors, fonts, animations, dividing line and so on
83
85
  - [Template Variables](https://jaywhj.netlify.app/document-dates-en#Template-Variables): Can be used to optimize `sitemap.xml` for site SEO
84
86
  - [Recently Updated Module](https://jaywhj.netlify.app/document-dates-en#Recently-Updated-Module): Enable list of recently updated documents (in descending order of update date), this is ideal for sites with a large number of documents, so that readers can quickly see what's new
85
- - [Localization Language](https://jaywhj.netlify.app/document-dates-en#Localization-Language): More localization languages for `timeago` and `tooltip`
86
- - [Development Stories](https://jaywhj.netlify.app/document-dates-en#Development-Stories): Describes the origin of the plug-in, the difficulties and solutions encountered in development, and the principles and directions of product design
87
+ - [Localization Language](https://jaywhj.netlify.app/document-dates-en#Localization-Language): More localization languages for `timeago` and `tooltip`
88
+ - [Developer API](https://jaywhj.netlify.app/document-dates-en#Developer-API): Provides a date data API for developers, making it easy to retrieve exact dates in other plugins or hooks
87
89
 
88
90
  See the documentation for details: https://jaywhj.netlify.app/document-dates-en
89
91
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mkdocs-document-dates"
7
- version = "3.7.4"
7
+ version = "3.8.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"
@@ -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.4",
7
+ version="3.8.1",
8
8
  author="Aaron Wang",
9
9
  author_email="aaronwqt@gmail.com",
10
10
  license="MIT",