mkdocs2confluence 0.9.0__tar.gz → 0.9.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.
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/PKG-INFO +1 -1
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/pyproject.toml +1 -1
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/PKG-INFO +1 -1
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/emitter/xhtml.py +24 -5
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/ir/nodes.py +13 -7
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/footer.py +42 -6
- mkdocs2confluence-0.9.1/tests/test_footer.py +217 -0
- mkdocs2confluence-0.9.0/tests/test_footer.py +0 -119
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/LICENSE +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/README.md +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/setup.cfg +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/cli.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/ir/document.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/loader/config.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/loader/nav.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/pdf/render.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preview/server.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/publisher/client.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/publisher/pipeline.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/command.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/comments.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/github.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/platform.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/state.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_cli.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_emitter.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_images.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_loader.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_mermaid.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_parser.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_pdf.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_preprocess.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_preview.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_publish_client.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_publish_config.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_publish_pipeline.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_server.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_sync_anchoring.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_sync_command.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_sync_comments.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_sync_github.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_sync_state.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/tests/test_treeutil.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mkdocs2confluence"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "GPL-3.0-or-later" }
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/emitter/xhtml.py
RENAMED
|
@@ -251,19 +251,38 @@ def _emit_front_matter(node: FrontMatter) -> str:
|
|
|
251
251
|
return "".join(parts)
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
def _xml_safe(text: str) -> str:
|
|
255
|
+
"""HTML-escape *text* and encode non-ASCII as XML numeric character references.
|
|
256
|
+
|
|
257
|
+
Confluence storage format is XML; raw non-ASCII bytes (e.g. UTF-8 encoded ·)
|
|
258
|
+
can be misinterpreted as Latin-1 by some Confluence versions, producing artefacts
|
|
259
|
+
like ``·``. Encoding everything outside ASCII as ``&#NNN;`` entities is safe
|
|
260
|
+
for all Confluence deployments.
|
|
261
|
+
"""
|
|
262
|
+
return html.escape(text).encode("ascii", "xmlcharrefreplace").decode()
|
|
263
|
+
|
|
264
|
+
|
|
254
265
|
def _emit_source_footer(node: SourceFooter) -> str:
|
|
255
|
-
"""Emit a
|
|
266
|
+
"""Emit a borderless panel macro with edit/history links and last-commit info."""
|
|
256
267
|
edit_href = html.escape(node.edit_url)
|
|
257
268
|
links = f'<a href="{edit_href}">Edit this page</a>'
|
|
258
269
|
if node.history_url:
|
|
259
270
|
hist_href = html.escape(node.history_url)
|
|
260
|
-
links += f'
|
|
271
|
+
links += f' · <a href="{hist_href}">View history</a>'
|
|
261
272
|
body = f"<p>{links}</p>\n"
|
|
262
|
-
if node.
|
|
263
|
-
|
|
273
|
+
if node.commit_sha or node.commit_summary:
|
|
274
|
+
if node.commit_sha and node.commit_url:
|
|
275
|
+
sha_href = html.escape(node.commit_url)
|
|
276
|
+
sha_part = f'<a href="{sha_href}">{html.escape(node.commit_sha)}</a>'
|
|
277
|
+
elif node.commit_sha:
|
|
278
|
+
sha_part = html.escape(node.commit_sha)
|
|
279
|
+
else:
|
|
280
|
+
sha_part = ""
|
|
281
|
+
summary_part = _xml_safe(node.commit_summary) if node.commit_summary else ""
|
|
282
|
+
detail = " · ".join(p for p in [sha_part, summary_part] if p)
|
|
283
|
+
body += f"<p><strong>Last commit:</strong> {detail}</p>\n"
|
|
264
284
|
return (
|
|
265
285
|
'<ac:structured-macro ac:name="panel">\n'
|
|
266
|
-
' <ac:parameter ac:name="title">Page source</ac:parameter>\n'
|
|
267
286
|
' <ac:parameter ac:name="borderStyle">solid</ac:parameter>\n'
|
|
268
287
|
" <ac:rich-text-body>\n"
|
|
269
288
|
f" {body}"
|
|
@@ -435,17 +435,23 @@ class SourceFooter(IRNode):
|
|
|
435
435
|
Emitted as a Confluence ``panel`` macro at the bottom of the page.
|
|
436
436
|
|
|
437
437
|
Attributes:
|
|
438
|
-
edit_url:
|
|
439
|
-
history_url:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
438
|
+
edit_url: URL to edit the source file (e.g. GitHub edit link).
|
|
439
|
+
history_url: URL to the file's commit history. ``None`` when it
|
|
440
|
+
cannot be derived from ``edit_url``.
|
|
441
|
+
commit_sha: Short commit hash (e.g. ``"abc1234"``). ``None`` when
|
|
442
|
+
git is unavailable or the file is untracked.
|
|
443
|
+
commit_url: URL to the specific commit on the VCS host. ``None``
|
|
444
|
+
when the URL cannot be derived.
|
|
445
|
+
commit_summary: Commit message, author, and relative date, e.g.
|
|
446
|
+
``"Fix typo · Jane · 2 days ago"``. ``None`` when git
|
|
447
|
+
is unavailable or the file is untracked.
|
|
444
448
|
"""
|
|
445
449
|
|
|
446
450
|
edit_url: str
|
|
447
451
|
history_url: str | None = None
|
|
448
|
-
|
|
452
|
+
commit_sha: str | None = None
|
|
453
|
+
commit_url: str | None = None
|
|
454
|
+
commit_summary: str | None = None
|
|
449
455
|
|
|
450
456
|
|
|
451
457
|
@dataclass(frozen=True)
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/footer.py
RENAMED
|
@@ -11,6 +11,9 @@ import subprocess
|
|
|
11
11
|
|
|
12
12
|
from mkdocs_to_confluence.ir.nodes import SourceFooter
|
|
13
13
|
|
|
14
|
+
# separator used in git --format to allow reliable splitting
|
|
15
|
+
_GIT_SEP = "\x1f" # ASCII unit separator — never appears in commit messages
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
def _derive_history_url(edit_url: str) -> str | None:
|
|
16
19
|
"""Derive a commit-history URL from a VCS edit URL.
|
|
@@ -25,18 +28,34 @@ def _derive_history_url(edit_url: str) -> str | None:
|
|
|
25
28
|
return None
|
|
26
29
|
|
|
27
30
|
|
|
28
|
-
def
|
|
29
|
-
"""
|
|
31
|
+
def _derive_commit_url(edit_url: str, sha: str) -> str | None:
|
|
32
|
+
"""Derive a direct commit URL from a VCS edit URL and a commit SHA.
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
Supports GitHub (``/edit/``) and GitLab (``/-/edit/``).
|
|
35
|
+
Returns ``None`` for any other URL shape.
|
|
36
|
+
"""
|
|
37
|
+
if "/-/edit/" in edit_url:
|
|
38
|
+
base = edit_url.split("/-/edit/")[0]
|
|
39
|
+
return f"{base}/-/commit/{sha}"
|
|
40
|
+
if "/edit/" in edit_url:
|
|
41
|
+
base = edit_url.split("/edit/")[0]
|
|
42
|
+
return f"{base}/commit/{sha}"
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _last_commit_info(abs_path: str) -> tuple[str, str] | None:
|
|
47
|
+
"""Return ``(short_sha, summary)`` for the last commit touching *abs_path*.
|
|
48
|
+
|
|
49
|
+
*summary* is ``"message · author · relative_date"``.
|
|
32
50
|
Returns ``None`` when git is unavailable, the path is untracked, or the
|
|
33
51
|
command fails for any reason.
|
|
34
52
|
"""
|
|
53
|
+
sep = _GIT_SEP
|
|
35
54
|
try:
|
|
36
55
|
result = subprocess.run(
|
|
37
56
|
[
|
|
38
57
|
"git", "log", "-1",
|
|
39
|
-
"--format=%h
|
|
58
|
+
f"--format=%h{sep}%s{sep}%an{sep}%ad",
|
|
40
59
|
"--date=relative",
|
|
41
60
|
"--",
|
|
42
61
|
abs_path,
|
|
@@ -46,7 +65,13 @@ def _last_commit(abs_path: str) -> str | None:
|
|
|
46
65
|
timeout=5,
|
|
47
66
|
)
|
|
48
67
|
output = result.stdout.strip()
|
|
49
|
-
|
|
68
|
+
if not output:
|
|
69
|
+
return None
|
|
70
|
+
parts = output.split(sep, 3)
|
|
71
|
+
if len(parts) < 4:
|
|
72
|
+
return None
|
|
73
|
+
sha, message, author, date = parts
|
|
74
|
+
return sha, f"{message} \u00b7 {author} \u00b7 {date}"
|
|
50
75
|
except Exception: # noqa: BLE001
|
|
51
76
|
return None
|
|
52
77
|
|
|
@@ -62,8 +87,19 @@ def build_source_footer(edit_url: str, abs_path: str) -> SourceFooter:
|
|
|
62
87
|
Absolute filesystem path to the source Markdown file. Used to
|
|
63
88
|
query ``git log`` for the last-commit summary.
|
|
64
89
|
"""
|
|
90
|
+
commit_info = _last_commit_info(abs_path)
|
|
91
|
+
if commit_info is not None:
|
|
92
|
+
sha, summary = commit_info
|
|
93
|
+
commit_url = _derive_commit_url(edit_url, sha)
|
|
94
|
+
else:
|
|
95
|
+
sha = None
|
|
96
|
+
summary = None
|
|
97
|
+
commit_url = None
|
|
98
|
+
|
|
65
99
|
return SourceFooter(
|
|
66
100
|
edit_url=edit_url,
|
|
67
101
|
history_url=_derive_history_url(edit_url),
|
|
68
|
-
|
|
102
|
+
commit_sha=sha,
|
|
103
|
+
commit_url=commit_url,
|
|
104
|
+
commit_summary=summary,
|
|
69
105
|
)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Tests for transforms/footer.py and the SourceFooter emitter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
8
|
+
from mkdocs_to_confluence.ir.nodes import SourceFooter
|
|
9
|
+
from mkdocs_to_confluence.transforms.footer import (
|
|
10
|
+
_derive_commit_url,
|
|
11
|
+
_derive_history_url,
|
|
12
|
+
_last_commit_info,
|
|
13
|
+
build_source_footer,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# ── _derive_history_url ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_derive_history_url_github():
|
|
20
|
+
url = "https://github.com/org/repo/edit/main/docs/guide/setup.md"
|
|
21
|
+
assert _derive_history_url(url) == "https://github.com/org/repo/commits/main/docs/guide/setup.md"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_derive_history_url_gitlab():
|
|
25
|
+
url = "https://gitlab.com/org/repo/-/edit/main/docs/guide/setup.md"
|
|
26
|
+
assert _derive_history_url(url) == "https://gitlab.com/org/repo/-/commits/main/docs/guide/setup.md"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_derive_history_url_unknown_returns_none():
|
|
30
|
+
url = "https://bitbucket.org/org/repo/src/main/docs/guide/setup.md"
|
|
31
|
+
assert _derive_history_url(url) is None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_derive_history_url_empty_string():
|
|
35
|
+
assert _derive_history_url("") is None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── _derive_commit_url ────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_derive_commit_url_github():
|
|
42
|
+
url = "https://github.com/org/repo/edit/main/docs/page.md"
|
|
43
|
+
assert _derive_commit_url(url, "abc1234") == "https://github.com/org/repo/commit/abc1234"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_derive_commit_url_gitlab():
|
|
47
|
+
url = "https://gitlab.com/org/repo/-/edit/main/docs/page.md"
|
|
48
|
+
assert _derive_commit_url(url, "abc1234") == "https://gitlab.com/org/repo/-/commit/abc1234"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_derive_commit_url_unknown_returns_none():
|
|
52
|
+
url = "https://bitbucket.org/org/repo/src/main/docs/page.md"
|
|
53
|
+
assert _derive_commit_url(url, "abc1234") is None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── _last_commit_info ─────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_last_commit_info_returns_sha_and_summary(tmp_path):
|
|
60
|
+
fake_file = str(tmp_path / "docs" / "page.md")
|
|
61
|
+
sep = "\x1f"
|
|
62
|
+
with patch("mkdocs_to_confluence.transforms.footer.subprocess.run") as mock_run:
|
|
63
|
+
mock_run.return_value.stdout = f"abc1234{sep}Fix typo{sep}Jane{sep}2 days ago\n"
|
|
64
|
+
result = _last_commit_info(fake_file)
|
|
65
|
+
assert result is not None
|
|
66
|
+
sha, summary = result
|
|
67
|
+
assert sha == "abc1234"
|
|
68
|
+
assert "Fix typo" in summary
|
|
69
|
+
assert "Jane" in summary
|
|
70
|
+
assert "2 days ago" in summary
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_last_commit_info_returns_none_when_empty(tmp_path):
|
|
74
|
+
fake_file = str(tmp_path / "docs" / "untracked.md")
|
|
75
|
+
with patch("mkdocs_to_confluence.transforms.footer.subprocess.run") as mock_run:
|
|
76
|
+
mock_run.return_value.stdout = ""
|
|
77
|
+
result = _last_commit_info(fake_file)
|
|
78
|
+
assert result is None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_last_commit_info_returns_none_on_exception(tmp_path):
|
|
82
|
+
fake_file = str(tmp_path / "docs" / "page.md")
|
|
83
|
+
with patch("mkdocs_to_confluence.transforms.footer.subprocess.run", side_effect=FileNotFoundError):
|
|
84
|
+
result = _last_commit_info(fake_file)
|
|
85
|
+
assert result is None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ── build_source_footer ───────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_build_source_footer_full(tmp_path):
|
|
92
|
+
edit_url = "https://github.com/org/repo/edit/main/docs/guide.md"
|
|
93
|
+
abs_path = str(tmp_path / "docs" / "guide.md")
|
|
94
|
+
with patch("mkdocs_to_confluence.transforms.footer._last_commit_info",
|
|
95
|
+
return_value=("abc1234", "Fix typo · Jane · 1 day ago")):
|
|
96
|
+
footer = build_source_footer(edit_url, abs_path)
|
|
97
|
+
assert footer.edit_url == edit_url
|
|
98
|
+
assert footer.history_url == "https://github.com/org/repo/commits/main/docs/guide.md"
|
|
99
|
+
assert footer.commit_sha == "abc1234"
|
|
100
|
+
assert footer.commit_url == "https://github.com/org/repo/commit/abc1234"
|
|
101
|
+
assert footer.commit_summary == "Fix typo · Jane · 1 day ago"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_build_source_footer_no_commit(tmp_path):
|
|
105
|
+
edit_url = "https://github.com/org/repo/edit/main/docs/guide.md"
|
|
106
|
+
abs_path = str(tmp_path / "docs" / "guide.md")
|
|
107
|
+
with patch("mkdocs_to_confluence.transforms.footer._last_commit_info", return_value=None):
|
|
108
|
+
footer = build_source_footer(edit_url, abs_path)
|
|
109
|
+
assert footer.commit_sha is None
|
|
110
|
+
assert footer.commit_url is None
|
|
111
|
+
assert footer.commit_summary is None
|
|
112
|
+
assert footer.history_url is not None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── _emit_source_footer ───────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_emit_footer_contains_edit_link():
|
|
119
|
+
footer = SourceFooter(
|
|
120
|
+
edit_url="https://github.com/org/repo/edit/main/docs/page.md",
|
|
121
|
+
history_url="https://github.com/org/repo/commits/main/docs/page.md",
|
|
122
|
+
commit_sha="abc1234",
|
|
123
|
+
commit_url="https://github.com/org/repo/commit/abc1234",
|
|
124
|
+
commit_summary="Fix typo · Jane · 2 days ago",
|
|
125
|
+
)
|
|
126
|
+
out = emit((footer,))
|
|
127
|
+
assert "Edit this page" in out
|
|
128
|
+
assert "View history" in out
|
|
129
|
+
assert "abc1234" in out
|
|
130
|
+
assert "Fix typo" in out
|
|
131
|
+
assert 'ac:name="panel"' in out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_emit_footer_sha_is_hyperlink():
|
|
135
|
+
footer = SourceFooter(
|
|
136
|
+
edit_url="https://github.com/org/repo/edit/main/docs/page.md",
|
|
137
|
+
history_url=None,
|
|
138
|
+
commit_sha="abc1234",
|
|
139
|
+
commit_url="https://github.com/org/repo/commit/abc1234",
|
|
140
|
+
commit_summary="Fix typo · Jane · today",
|
|
141
|
+
)
|
|
142
|
+
out = emit((footer,))
|
|
143
|
+
assert 'href="https://github.com/org/repo/commit/abc1234"' in out
|
|
144
|
+
assert ">abc1234<" in out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_emit_footer_last_commit_bold():
|
|
148
|
+
footer = SourceFooter(
|
|
149
|
+
edit_url="https://github.com/org/repo/edit/main/docs/page.md",
|
|
150
|
+
history_url=None,
|
|
151
|
+
commit_sha="abc1234",
|
|
152
|
+
commit_url="https://github.com/org/repo/commit/abc1234",
|
|
153
|
+
commit_summary="Fix typo · Jane · today",
|
|
154
|
+
)
|
|
155
|
+
out = emit((footer,))
|
|
156
|
+
assert "<strong>Last commit:</strong>" in out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_emit_footer_no_panel_title():
|
|
160
|
+
footer = SourceFooter(
|
|
161
|
+
edit_url="https://github.com/org/repo/edit/main/docs/page.md",
|
|
162
|
+
)
|
|
163
|
+
out = emit((footer,))
|
|
164
|
+
assert 'ac:name="title"' not in out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_emit_footer_no_history_url():
|
|
168
|
+
footer = SourceFooter(
|
|
169
|
+
edit_url="https://example.com/edit/docs/page.md",
|
|
170
|
+
history_url=None,
|
|
171
|
+
commit_sha=None,
|
|
172
|
+
commit_url=None,
|
|
173
|
+
commit_summary=None,
|
|
174
|
+
)
|
|
175
|
+
out = emit((footer,))
|
|
176
|
+
assert "Edit this page" in out
|
|
177
|
+
assert "View history" not in out
|
|
178
|
+
assert "Last commit" not in out
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_emit_footer_escapes_html():
|
|
182
|
+
footer = SourceFooter(
|
|
183
|
+
edit_url='https://example.com/edit/path?a=1&b=2',
|
|
184
|
+
history_url=None,
|
|
185
|
+
commit_sha=None,
|
|
186
|
+
commit_url=None,
|
|
187
|
+
commit_summary='<script>alert(1)</script> · Jane · today',
|
|
188
|
+
)
|
|
189
|
+
out = emit((footer,))
|
|
190
|
+
assert "<script>" not in out
|
|
191
|
+
assert "<script>" in out
|
|
192
|
+
assert "a=1&b=2" in out
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_emit_footer_non_ascii_encoded_as_entity():
|
|
196
|
+
"""Non-ASCII characters in commit_summary are encoded as XML numeric entities."""
|
|
197
|
+
footer = SourceFooter(
|
|
198
|
+
edit_url="https://github.com/org/repo/edit/main/docs/page.md",
|
|
199
|
+
history_url=None,
|
|
200
|
+
commit_sha=None,
|
|
201
|
+
commit_url=None,
|
|
202
|
+
commit_summary="Fix caf\u00e9 · \u00c5ngstr\u00f6m · 2 days ago",
|
|
203
|
+
)
|
|
204
|
+
out = emit((footer,))
|
|
205
|
+
assert "caf\u00e9" not in out
|
|
206
|
+
assert "é" in out or "é" in out.lower()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_emit_footer_separator_is_entity():
|
|
210
|
+
"""The · separator between Edit/View history links is an XML entity, not raw UTF-8."""
|
|
211
|
+
footer = SourceFooter(
|
|
212
|
+
edit_url="https://github.com/org/repo/edit/main/docs/page.md",
|
|
213
|
+
history_url="https://github.com/org/repo/commits/main/docs/page.md",
|
|
214
|
+
)
|
|
215
|
+
out = emit((footer,))
|
|
216
|
+
assert "·" in out
|
|
217
|
+
assert "\u00b7" not in out
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"""Tests for transforms/footer.py and the SourceFooter emitter."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from unittest.mock import patch
|
|
6
|
-
|
|
7
|
-
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
8
|
-
from mkdocs_to_confluence.ir.nodes import SourceFooter
|
|
9
|
-
from mkdocs_to_confluence.transforms.footer import _derive_history_url, _last_commit, build_source_footer
|
|
10
|
-
|
|
11
|
-
# ── _derive_history_url ───────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_derive_history_url_github():
|
|
15
|
-
url = "https://github.com/org/repo/edit/main/docs/guide/setup.md"
|
|
16
|
-
assert _derive_history_url(url) == "https://github.com/org/repo/commits/main/docs/guide/setup.md"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_derive_history_url_gitlab():
|
|
20
|
-
url = "https://gitlab.com/org/repo/-/edit/main/docs/guide/setup.md"
|
|
21
|
-
assert _derive_history_url(url) == "https://gitlab.com/org/repo/-/commits/main/docs/guide/setup.md"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_derive_history_url_unknown_returns_none():
|
|
25
|
-
url = "https://bitbucket.org/org/repo/src/main/docs/guide/setup.md"
|
|
26
|
-
assert _derive_history_url(url) is None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_derive_history_url_empty_string():
|
|
30
|
-
assert _derive_history_url("") is None
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# ── _last_commit ──────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_last_commit_returns_output(tmp_path):
|
|
37
|
-
fake_file = str(tmp_path / "docs" / "page.md")
|
|
38
|
-
with patch("mkdocs_to_confluence.transforms.footer.subprocess.run") as mock_run:
|
|
39
|
-
mock_run.return_value.stdout = "abc1234 · Fix typo · Jane · 2 days ago\n"
|
|
40
|
-
result = _last_commit(fake_file)
|
|
41
|
-
assert result == "abc1234 · Fix typo · Jane · 2 days ago"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_last_commit_returns_none_when_empty(tmp_path):
|
|
45
|
-
fake_file = str(tmp_path / "docs" / "untracked.md")
|
|
46
|
-
with patch("mkdocs_to_confluence.transforms.footer.subprocess.run") as mock_run:
|
|
47
|
-
mock_run.return_value.stdout = ""
|
|
48
|
-
result = _last_commit(fake_file)
|
|
49
|
-
assert result is None
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_last_commit_returns_none_on_exception(tmp_path):
|
|
53
|
-
fake_file = str(tmp_path / "docs" / "page.md")
|
|
54
|
-
with patch("mkdocs_to_confluence.transforms.footer.subprocess.run", side_effect=FileNotFoundError):
|
|
55
|
-
result = _last_commit(fake_file)
|
|
56
|
-
assert result is None
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# ── build_source_footer ───────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def test_build_source_footer_full(tmp_path):
|
|
63
|
-
edit_url = "https://github.com/org/repo/edit/main/docs/guide.md"
|
|
64
|
-
abs_path = str(tmp_path / "docs" / "guide.md")
|
|
65
|
-
with patch("mkdocs_to_confluence.transforms.footer._last_commit", return_value="abc · msg · Jane · 1 day ago"):
|
|
66
|
-
footer = build_source_footer(edit_url, abs_path)
|
|
67
|
-
assert footer.edit_url == edit_url
|
|
68
|
-
assert footer.history_url == "https://github.com/org/repo/commits/main/docs/guide.md"
|
|
69
|
-
assert footer.last_commit == "abc · msg · Jane · 1 day ago"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def test_build_source_footer_no_commit(tmp_path):
|
|
73
|
-
edit_url = "https://github.com/org/repo/edit/main/docs/guide.md"
|
|
74
|
-
abs_path = str(tmp_path / "docs" / "guide.md")
|
|
75
|
-
with patch("mkdocs_to_confluence.transforms.footer._last_commit", return_value=None):
|
|
76
|
-
footer = build_source_footer(edit_url, abs_path)
|
|
77
|
-
assert footer.last_commit is None
|
|
78
|
-
assert footer.history_url is not None
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# ── _emit_source_footer ───────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def test_emit_footer_contains_edit_link():
|
|
85
|
-
footer = SourceFooter(
|
|
86
|
-
edit_url="https://github.com/org/repo/edit/main/docs/page.md",
|
|
87
|
-
history_url="https://github.com/org/repo/commits/main/docs/page.md",
|
|
88
|
-
last_commit="abc1234 · Fix typo · Jane · 2 days ago",
|
|
89
|
-
)
|
|
90
|
-
out = emit((footer,))
|
|
91
|
-
assert "Edit this page" in out
|
|
92
|
-
assert "View history" in out
|
|
93
|
-
assert "abc1234" in out
|
|
94
|
-
assert "Fix typo" in out
|
|
95
|
-
assert 'ac:name="panel"' in out
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def test_emit_footer_no_history_url():
|
|
99
|
-
footer = SourceFooter(
|
|
100
|
-
edit_url="https://example.com/edit/docs/page.md",
|
|
101
|
-
history_url=None,
|
|
102
|
-
last_commit=None,
|
|
103
|
-
)
|
|
104
|
-
out = emit((footer,))
|
|
105
|
-
assert "Edit this page" in out
|
|
106
|
-
assert "View history" not in out
|
|
107
|
-
assert "Last commit" not in out
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_emit_footer_escapes_html():
|
|
111
|
-
footer = SourceFooter(
|
|
112
|
-
edit_url='https://example.com/edit/path?a=1&b=2',
|
|
113
|
-
history_url=None,
|
|
114
|
-
last_commit='abc · <script>alert(1)</script> · Jane · today',
|
|
115
|
-
)
|
|
116
|
-
out = emit((footer,))
|
|
117
|
-
assert "<script>" not in out
|
|
118
|
-
assert "<script>" in out
|
|
119
|
-
assert "a=1&b=2" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/requires.txt
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs2confluence.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/emitter/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/loader/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/loader/config.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/loader/extra_css.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/parser/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/parser/markdown.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/pdf/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/pdf/generator.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/abbrevs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/fence.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/icons.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/includes.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preprocess/linkdefs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preview/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preview/render.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/preview/server.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/publisher/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/publisher/client.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/publisher/pipeline.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/anchoring.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/command.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/comments.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/sync/platform.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/abbrevs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/assets.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/editlink.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/images.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.1}/src/mkdocs_to_confluence/transforms/mermaid.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
|
|
File without changes
|