mkdocs2confluence 0.9.0__tar.gz → 0.9.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.
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/PKG-INFO +1 -1
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/pyproject.toml +2 -1
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/PKG-INFO +1 -1
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/cli.py +4 -4
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/emitter/xhtml.py +24 -5
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/ir/nodes.py +13 -7
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/publisher/client.py +2 -1
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/publisher/pipeline.py +16 -8
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/footer.py +42 -6
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_cli.py +2 -2
- mkdocs2confluence-0.9.2/tests/test_footer.py +217 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_publish_pipeline.py +8 -8
- mkdocs2confluence-0.9.0/tests/test_footer.py +0 -119
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/LICENSE +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/README.md +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/setup.cfg +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/ir/document.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/loader/config.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/loader/nav.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/pdf/render.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preview/server.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/command.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/comments.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/github.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/platform.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/state.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_emitter.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_images.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_loader.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_mermaid.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_parser.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_pdf.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_preprocess.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_preview.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_publish_client.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_publish_config.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_server.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_sync_anchoring.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_sync_command.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_sync_comments.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_sync_github.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/tests/test_sync_state.py +0 -0
- {mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/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.2"
|
|
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" }
|
|
@@ -73,6 +73,7 @@ select = ["E", "F", "I"]
|
|
|
73
73
|
|
|
74
74
|
[tool.mypy]
|
|
75
75
|
strict = true
|
|
76
|
+
extra_checks = true
|
|
76
77
|
python_version = "3.12"
|
|
77
78
|
|
|
78
79
|
[[tool.mypy.overrides]]
|
|
@@ -359,7 +359,7 @@ def _cmd_preview(args: argparse.Namespace) -> None:
|
|
|
359
359
|
for node in pages:
|
|
360
360
|
html_name = page_link_map.get(node.title, f"{Path(node.docs_path or node.title).stem}.html")
|
|
361
361
|
try:
|
|
362
|
-
xhtml, _a, _l, _s = compile_page(node, config, link_map, quiet=args.quiet)
|
|
362
|
+
xhtml, _a, _l, _s, _vm = compile_page(node, config, link_map, quiet=args.quiet)
|
|
363
363
|
except PageLoadError as exc:
|
|
364
364
|
print(f" warning: skipping '{node.title}': {exc}", file=sys.stderr)
|
|
365
365
|
continue
|
|
@@ -417,7 +417,7 @@ def _cmd_preview(args: argparse.Namespace) -> None:
|
|
|
417
417
|
|
|
418
418
|
def _build_page() -> None:
|
|
419
419
|
try:
|
|
420
|
-
xhtml, _a, _l, _s = compile_page(page_node, config, link_map, quiet=True)
|
|
420
|
+
xhtml, _a, _l, _s, _vm = compile_page(page_node, config, link_map, quiet=True)
|
|
421
421
|
except PageLoadError as exc:
|
|
422
422
|
print(f" warning: {exc}", file=sys.stderr)
|
|
423
423
|
return
|
|
@@ -441,7 +441,7 @@ def _cmd_preview(args: argparse.Namespace) -> None:
|
|
|
441
441
|
return
|
|
442
442
|
|
|
443
443
|
try:
|
|
444
|
-
xhtml, _attachments, _labels, _status = compile_page(page_node, config, link_map, quiet=args.quiet)
|
|
444
|
+
xhtml, _attachments, _labels, _status, _vm = compile_page(page_node, config, link_map, quiet=args.quiet)
|
|
445
445
|
except PageLoadError as exc:
|
|
446
446
|
print(f"error: {exc}", file=sys.stderr)
|
|
447
447
|
sys.exit(1)
|
|
@@ -656,7 +656,7 @@ def _cmd_pdf(args: argparse.Namespace) -> None:
|
|
|
656
656
|
chapters: list[tuple[str, str]] = []
|
|
657
657
|
for node in pages:
|
|
658
658
|
try:
|
|
659
|
-
xhtml, _a, _l, _s = compile_page(node, config, link_map, quiet=args.quiet)
|
|
659
|
+
xhtml, _a, _l, _s, _vm = compile_page(node, config, link_map, quiet=args.quiet)
|
|
660
660
|
except PageLoadError as exc:
|
|
661
661
|
print(f" warning: skipping '{node.title}': {exc}", file=sys.stderr)
|
|
662
662
|
continue
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/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.2}/src/mkdocs_to_confluence/publisher/client.py
RENAMED
|
@@ -255,6 +255,7 @@ class ConfluenceClient:
|
|
|
255
255
|
version: int,
|
|
256
256
|
*,
|
|
257
257
|
parent_id: str | None = None,
|
|
258
|
+
version_message: str | None = None,
|
|
258
259
|
) -> dict[str, Any]:
|
|
259
260
|
"""Update an existing page to a new version and return the page dict.
|
|
260
261
|
|
|
@@ -273,7 +274,7 @@ class ConfluenceClient:
|
|
|
273
274
|
},
|
|
274
275
|
"version": {
|
|
275
276
|
"number": version,
|
|
276
|
-
"message": "
|
|
277
|
+
"message": version_message or "Published by mk2conf",
|
|
277
278
|
"minorEdit": True,
|
|
278
279
|
},
|
|
279
280
|
}
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/publisher/pipeline.py
RENAMED
|
@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Literal
|
|
|
24
24
|
import yaml
|
|
25
25
|
|
|
26
26
|
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
27
|
-
from mkdocs_to_confluence.ir.nodes import FrontMatter
|
|
27
|
+
from mkdocs_to_confluence.ir.nodes import FrontMatter, SourceFooter
|
|
28
28
|
from mkdocs_to_confluence.loader.config import ConfluenceConfig, MkDocsConfig
|
|
29
29
|
from mkdocs_to_confluence.loader.nav import NavNode
|
|
30
30
|
from mkdocs_to_confluence.loader.page import PageLoadError, load_page
|
|
@@ -75,6 +75,7 @@ class PageAction:
|
|
|
75
75
|
attachments: list[Path] = field(default_factory=list)
|
|
76
76
|
labels: tuple[str, ...] = field(default_factory=tuple)
|
|
77
77
|
confluence_status: str | None = None
|
|
78
|
+
version_message: str | None = None # git commit message for Confluence version history
|
|
78
79
|
is_folder: bool = False # True when this action creates a Confluence folder
|
|
79
80
|
parent_is_folder: bool = False # True when the parent content is a folder
|
|
80
81
|
# Set after execution:
|
|
@@ -139,7 +140,7 @@ def compile_page(
|
|
|
139
140
|
link_map: dict[str, str] | None = None,
|
|
140
141
|
*,
|
|
141
142
|
quiet: bool = False,
|
|
142
|
-
) -> tuple[str, list[Path], tuple[str, ...], str | None]:
|
|
143
|
+
) -> tuple[str, list[Path], tuple[str, ...], str | None, str | None]:
|
|
143
144
|
"""Run the full compile pipeline for one page.
|
|
144
145
|
|
|
145
146
|
Returns
|
|
@@ -148,7 +149,7 @@ def compile_page(
|
|
|
148
149
|
``(xhtml_string, attachment_paths, labels, confluence_status)``
|
|
149
150
|
"""
|
|
150
151
|
if node.source_path is None:
|
|
151
|
-
return "", [], (), None
|
|
152
|
+
return "", [], (), None, None
|
|
152
153
|
|
|
153
154
|
raw = load_page(node)
|
|
154
155
|
|
|
@@ -195,17 +196,19 @@ def compile_page(
|
|
|
195
196
|
footer = build_source_footer(edit_url, abs_path)
|
|
196
197
|
ir_nodes = ir_nodes + (footer,)
|
|
197
198
|
|
|
198
|
-
# Extract labels and
|
|
199
|
+
# Extract labels, confluence_status, and version_message from IR nodes.
|
|
199
200
|
labels: tuple[str, ...] = ()
|
|
200
201
|
confluence_status: str | None = None
|
|
202
|
+
version_message: str | None = None
|
|
201
203
|
for node_item in ir_nodes:
|
|
202
204
|
if isinstance(node_item, FrontMatter):
|
|
203
205
|
labels = node_item.labels
|
|
204
206
|
confluence_status = node_item.confluence_status
|
|
205
|
-
|
|
207
|
+
if isinstance(node_item, SourceFooter) and node_item.commit_sha and node_item.commit_summary:
|
|
208
|
+
version_message = f"{node_item.commit_sha}: {node_item.commit_summary}"
|
|
206
209
|
|
|
207
210
|
xhtml = emit(ir_nodes)
|
|
208
|
-
return xhtml, attachments, labels, confluence_status
|
|
211
|
+
return xhtml, attachments, labels, confluence_status, version_message
|
|
209
212
|
|
|
210
213
|
|
|
211
214
|
def _xhtml_hash(xhtml: str) -> str:
|
|
@@ -279,7 +282,7 @@ def _plan_nodes(
|
|
|
279
282
|
if not quiet:
|
|
280
283
|
print(f" compiling '{clean_title}' (section index)")
|
|
281
284
|
try:
|
|
282
|
-
xhtml, attachments, labels, confluence_status = compile_page(
|
|
285
|
+
xhtml, attachments, labels, confluence_status, version_message = compile_page(
|
|
283
286
|
index_child, config, link_map, quiet=quiet
|
|
284
287
|
)
|
|
285
288
|
existing = client.find_page(space_id, clean_title)
|
|
@@ -312,6 +315,7 @@ def _plan_nodes(
|
|
|
312
315
|
attachments=attachments,
|
|
313
316
|
labels=labels,
|
|
314
317
|
confluence_status=confluence_status,
|
|
318
|
+
version_message=version_message,
|
|
315
319
|
page_id=str(existing["id"]) if existing is not None else None,
|
|
316
320
|
version=(
|
|
317
321
|
existing["version"]["number"] if existing is not None else None
|
|
@@ -379,7 +383,9 @@ def _plan_nodes(
|
|
|
379
383
|
if not quiet:
|
|
380
384
|
print(f" compiling '{clean_title}'")
|
|
381
385
|
try:
|
|
382
|
-
xhtml, attachments, labels, confluence_status = compile_page(
|
|
386
|
+
xhtml, attachments, labels, confluence_status, version_message = compile_page(
|
|
387
|
+
node, config, link_map, quiet=quiet
|
|
388
|
+
)
|
|
383
389
|
except (PageLoadError, OSError) as exc:
|
|
384
390
|
if not quiet:
|
|
385
391
|
print(f" skipping '{clean_title}' (error: {exc})")
|
|
@@ -419,6 +425,7 @@ def _plan_nodes(
|
|
|
419
425
|
attachments=attachments,
|
|
420
426
|
labels=labels,
|
|
421
427
|
confluence_status=confluence_status,
|
|
428
|
+
version_message=version_message,
|
|
422
429
|
page_id=str(existing["id"]) if existing is not None else None,
|
|
423
430
|
version=(
|
|
424
431
|
existing["version"]["number"] if existing is not None else None
|
|
@@ -588,6 +595,7 @@ def _execute_page_action(
|
|
|
588
595
|
action.xhtml or "",
|
|
589
596
|
action.version + 1,
|
|
590
597
|
parent_id=action.parent_id,
|
|
598
|
+
version_message=action.version_message,
|
|
591
599
|
)
|
|
592
600
|
report.updated += 1
|
|
593
601
|
except ConfluenceError as upd_exc:
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/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
|
)
|
|
@@ -175,7 +175,7 @@ class TestQuietOutputBehavior:
|
|
|
175
175
|
yml = _minimal_config(tmp_path)
|
|
176
176
|
(tmp_path / "docs" / "index.md").write_text("# Home\n\nHello.", encoding="utf-8")
|
|
177
177
|
|
|
178
|
-
mock_compile = MagicMock(return_value=("<p>Hello</p>", [], (), None))
|
|
178
|
+
mock_compile = MagicMock(return_value=("<p>Hello</p>", [], (), None, None))
|
|
179
179
|
with patch("mkdocs_to_confluence.cli.compile_page", mock_compile), \
|
|
180
180
|
patch("sys.stdout.isatty", return_value=False):
|
|
181
181
|
flags = ["--quiet"] if quiet else []
|
|
@@ -223,7 +223,7 @@ class TestWatchFlag:
|
|
|
223
223
|
yml = _minimal_config(tmp_path)
|
|
224
224
|
(tmp_path / "docs" / "index.md").write_text("# Home\n", encoding="utf-8")
|
|
225
225
|
|
|
226
|
-
mock_compile = MagicMock(return_value=("<p>Hello</p>", [], (), None))
|
|
226
|
+
mock_compile = MagicMock(return_value=("<p>Hello</p>", [], (), None, None))
|
|
227
227
|
mock_render = MagicMock(return_value="<html>preview</html>")
|
|
228
228
|
|
|
229
229
|
with patch("mkdocs_to_confluence.cli.compile_page", mock_compile), \
|
|
@@ -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
|
|
@@ -53,7 +53,7 @@ def test_compile_page_returns_xhtml(tmp_path: Path) -> None:
|
|
|
53
53
|
|
|
54
54
|
node = _page_node("Index", md)
|
|
55
55
|
config = _make_config(docs)
|
|
56
|
-
xhtml, attachments, labels, _ = compile_page(node, config)
|
|
56
|
+
xhtml, attachments, labels, _, _ = compile_page(node, config)
|
|
57
57
|
|
|
58
58
|
assert "<h1>" in xhtml or "Hello" in xhtml
|
|
59
59
|
assert attachments == []
|
|
@@ -68,7 +68,7 @@ def test_compile_page_with_ready_false_still_compiles(tmp_path: Path) -> None:
|
|
|
68
68
|
|
|
69
69
|
node = _page_node("Draft", md)
|
|
70
70
|
config = _make_config(docs)
|
|
71
|
-
xhtml, attachments, labels, _ = compile_page(node, config)
|
|
71
|
+
xhtml, attachments, labels, _, _ = compile_page(node, config)
|
|
72
72
|
# Still compiles fine; plan_publish is the gatekeeper
|
|
73
73
|
assert isinstance(xhtml, str)
|
|
74
74
|
|
|
@@ -78,7 +78,7 @@ def test_compile_page_with_source_path_none_returns_empty(tmp_path: Path) -> Non
|
|
|
78
78
|
docs.mkdir()
|
|
79
79
|
node = NavNode(title="Missing", docs_path="missing.md", source_path=None, level=0)
|
|
80
80
|
config = _make_config(docs)
|
|
81
|
-
xhtml, attachments, labels, _ = compile_page(node, config)
|
|
81
|
+
xhtml, attachments, labels, _, _ = compile_page(node, config)
|
|
82
82
|
assert xhtml == ""
|
|
83
83
|
assert attachments == []
|
|
84
84
|
assert labels == ()
|
|
@@ -179,7 +179,7 @@ def test_plan_publish_skips_when_content_unchanged(tmp_path: Path) -> None:
|
|
|
179
179
|
|
|
180
180
|
# Compile once to get the real hash
|
|
181
181
|
from mkdocs_to_confluence.publisher.pipeline import compile_page
|
|
182
|
-
xhtml, _, _, _ = compile_page(node, config)
|
|
182
|
+
xhtml, _, _, _, _ = compile_page(node, config)
|
|
183
183
|
stored_hash = _xhtml_hash(xhtml)
|
|
184
184
|
|
|
185
185
|
existing_page = {"id": "77", "version": {"number": 2}}
|
|
@@ -1672,7 +1672,7 @@ class TestExecutePublishHelpers:
|
|
|
1672
1672
|
|
|
1673
1673
|
assert report.updated == 1
|
|
1674
1674
|
client.update_page.assert_called_once_with(
|
|
1675
|
-
"existing-9", "P", "<p/>", 4, parent_id="ROOT"
|
|
1675
|
+
"existing-9", "P", "<p/>", 4, parent_id="ROOT", version_message=None
|
|
1676
1676
|
)
|
|
1677
1677
|
client.create_page.assert_not_called()
|
|
1678
1678
|
|
|
@@ -2036,7 +2036,7 @@ def test_compile_page_returns_confluence_status(tmp_path: Path) -> None:
|
|
|
2036
2036
|
|
|
2037
2037
|
node = _page_node("My Page", md)
|
|
2038
2038
|
config = _make_config(docs)
|
|
2039
|
-
_, _, _, confluence_status = compile_page(node, config)
|
|
2039
|
+
_, _, _, confluence_status, _ = compile_page(node, config)
|
|
2040
2040
|
|
|
2041
2041
|
assert confluence_status == "in-progress"
|
|
2042
2042
|
|
|
@@ -2060,7 +2060,7 @@ def test_compile_page_returns_confluence_status_with_repo_url(tmp_path: Path) ->
|
|
|
2060
2060
|
edit_uri="edit/main/docs/",
|
|
2061
2061
|
nav=None,
|
|
2062
2062
|
)
|
|
2063
|
-
_, _, _, confluence_status = compile_page(node, config)
|
|
2063
|
+
_, _, _, confluence_status, _ = compile_page(node, config)
|
|
2064
2064
|
|
|
2065
2065
|
assert confluence_status == "in-progress"
|
|
2066
2066
|
|
|
@@ -2098,7 +2098,7 @@ def test_plan_publish_sets_confluence_status_on_skip(tmp_path: Path) -> None:
|
|
|
2098
2098
|
config = _make_config(docs)
|
|
2099
2099
|
conf_config = _make_conf_config()
|
|
2100
2100
|
|
|
2101
|
-
xhtml, _, _, _ = compile_page(node, config)
|
|
2101
|
+
xhtml, _, _, _, _ = compile_page(node, config)
|
|
2102
2102
|
stored_hash = _xhtml_hash(xhtml)
|
|
2103
2103
|
|
|
2104
2104
|
existing_page = {"id": "77", "version": {"number": 2}}
|
|
@@ -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.2}/src/mkdocs2confluence.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/requires.txt
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs2confluence.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/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.2}/src/mkdocs_to_confluence/loader/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/loader/config.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/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.2}/src/mkdocs_to_confluence/parser/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/parser/markdown.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/pdf/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/pdf/generator.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/abbrevs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/fence.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/icons.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/includes.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preprocess/linkdefs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preview/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preview/render.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/preview/server.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/publisher/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/anchoring.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/command.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/comments.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/sync/platform.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/abbrevs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/assets.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/editlink.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/src/mkdocs_to_confluence/transforms/images.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.0 → mkdocs2confluence-0.9.2}/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
|