mkdocs2confluence 0.9.1__tar.gz → 0.9.3__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 (87) hide show
  1. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/PKG-INFO +6 -2
  2. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/README.md +5 -1
  3. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/pyproject.toml +2 -1
  4. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs2confluence.egg-info/PKG-INFO +6 -2
  5. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs2confluence.egg-info/SOURCES.txt +1 -0
  6. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/cli.py +4 -4
  7. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/emitter/xhtml.py +12 -0
  8. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/ir/nodes.py +9 -0
  9. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/publisher/client.py +2 -1
  10. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/publisher/pipeline.py +20 -9
  11. mkdocs2confluence-0.9.3/tests/test_children_macro.py +107 -0
  12. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_cli.py +2 -2
  13. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_publish_pipeline.py +8 -8
  14. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/LICENSE +0 -0
  15. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/setup.cfg +0 -0
  16. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  17. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  18. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  19. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  20. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/__init__.py +0 -0
  21. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  22. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  23. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/ir/document.py +0 -0
  24. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  25. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  26. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/loader/config.py +0 -0
  27. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  28. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  29. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/loader/page.py +0 -0
  30. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  31. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  32. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  33. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  34. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  35. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  36. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  37. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  38. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  39. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  40. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  41. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  42. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  43. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preview/render.py +0 -0
  44. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/preview/server.py +0 -0
  45. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  46. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
  47. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
  48. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/sync/command.py +0 -0
  49. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/sync/comments.py +0 -0
  50. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/sync/github.py +0 -0
  51. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/sync/platform.py +0 -0
  52. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/sync/state.py +0 -0
  53. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  54. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  55. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  56. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  57. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
  58. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  59. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  60. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  61. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_abbrevs.py +0 -0
  62. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_editlink.py +0 -0
  63. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_emitter.py +0 -0
  64. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_extra_css.py +0 -0
  65. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_footer.py +0 -0
  66. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_frontmatter.py +0 -0
  67. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_icons.py +0 -0
  68. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_images.py +0 -0
  69. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_internallinks.py +0 -0
  70. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_ir.py +0 -0
  71. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_linkdefs.py +0 -0
  72. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_loader.py +0 -0
  73. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_mermaid.py +0 -0
  74. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_page_loader.py +0 -0
  75. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_parser.py +0 -0
  76. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_pdf.py +0 -0
  77. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_preprocess.py +0 -0
  78. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_preview.py +0 -0
  79. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_publish_client.py +0 -0
  80. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_publish_config.py +0 -0
  81. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_server.py +0 -0
  82. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_sync_anchoring.py +0 -0
  83. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_sync_command.py +0 -0
  84. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_sync_comments.py +0 -0
  85. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_sync_github.py +0 -0
  86. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_sync_state.py +0 -0
  87. {mkdocs2confluence-0.9.1 → mkdocs2confluence-0.9.3}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.9.1
3
+ Version: 0.9.3
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -431,7 +431,11 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
431
431
 
432
432
  - **Edit this page** — links to the source file in your VCS (GitHub/GitLab/etc.)
433
433
  - **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
434
- - **Last commit** — short commit SHA, message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
434
+ - **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
435
+
436
+ ### Section index child pages
437
+
438
+ When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
435
439
 
436
440
  ### Abbreviation expansion
437
441
 
@@ -391,7 +391,11 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
391
391
 
392
392
  - **Edit this page** — links to the source file in your VCS (GitHub/GitLab/etc.)
393
393
  - **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
394
- - **Last commit** — short commit SHA, message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
394
+ - **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
395
+
396
+ ### Section index child pages
397
+
398
+ When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
395
399
 
396
400
  ### Abbreviation expansion
397
401
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.9.1"
3
+ version = "0.9.3"
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]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.9.1
3
+ Version: 0.9.3
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -431,7 +431,11 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
431
431
 
432
432
  - **Edit this page** — links to the source file in your VCS (GitHub/GitLab/etc.)
433
433
  - **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
434
- - **Last commit** — short commit SHA, message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
434
+ - **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
435
+
436
+ ### Section index child pages
437
+
438
+ When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
435
439
 
436
440
  ### Abbreviation expansion
437
441
 
@@ -54,6 +54,7 @@ src/mkdocs_to_confluence/transforms/images.py
54
54
  src/mkdocs_to_confluence/transforms/internallinks.py
55
55
  src/mkdocs_to_confluence/transforms/mermaid.py
56
56
  tests/test_abbrevs.py
57
+ tests/test_children_macro.py
57
58
  tests/test_cli.py
58
59
  tests/test_editlink.py
59
60
  tests/test_emitter.py
@@ -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
@@ -27,6 +27,7 @@ from mkdocs_to_confluence.ir.nodes import (
27
27
  BlockQuote,
28
28
  BoldNode,
29
29
  BulletList,
30
+ ChildrenMacro,
30
31
  CodeBlock,
31
32
  CodeInlineNode,
32
33
  ContentTabs,
@@ -198,6 +199,8 @@ def _emit_node(node: IRNode) -> str:
198
199
  return _emit_grid_cards(node)
199
200
  if isinstance(node, SourceFooter):
200
201
  return _emit_source_footer(node)
202
+ if isinstance(node, ChildrenMacro):
203
+ return _emit_children_macro()
201
204
  if isinstance(node, UnsupportedBlock):
202
205
  return _emit_unsupported(node)
203
206
  # Inline nodes at block level (shouldn't normally appear, but be safe)
@@ -262,6 +265,15 @@ def _xml_safe(text: str) -> str:
262
265
  return html.escape(text).encode("ascii", "xmlcharrefreplace").decode()
263
266
 
264
267
 
268
+ def _emit_children_macro() -> str:
269
+ """Emit the Confluence Children Display macro for section index pages."""
270
+ return (
271
+ '<ac:structured-macro ac:name="children">'
272
+ '<ac:parameter ac:name="depth">1</ac:parameter>'
273
+ "</ac:structured-macro>"
274
+ )
275
+
276
+
265
277
  def _emit_source_footer(node: SourceFooter) -> str:
266
278
  """Emit a borderless panel macro with edit/history links and last-commit info."""
267
279
  edit_href = html.escape(node.edit_url)
@@ -428,6 +428,15 @@ class FrontMatter(IRNode):
428
428
  # ── Abbreviation footnotes ────────────────────────────────────────────────────
429
429
 
430
430
 
431
+ @dataclass(frozen=True)
432
+ class ChildrenMacro(IRNode):
433
+ """Confluence Children Display macro listing direct child pages.
434
+
435
+ Appended to section index pages so readers see a live, auto-maintained
436
+ sub-page list. Emitted as ``ac:structured-macro ac:name="children"``.
437
+ """
438
+
439
+
431
440
  @dataclass(frozen=True)
432
441
  class SourceFooter(IRNode):
433
442
  """Footer panel showing source-control links and last-commit info.
@@ -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": "Updated by mk2conf",
277
+ "message": version_message or "Published by mk2conf",
277
278
  "minorEdit": True,
278
279
  },
279
280
  }
@@ -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 ChildrenMacro, 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:
@@ -138,8 +139,9 @@ def compile_page(
138
139
  config: MkDocsConfig,
139
140
  link_map: dict[str, str] | None = None,
140
141
  *,
142
+ is_section_index: bool = False,
141
143
  quiet: bool = False,
142
- ) -> tuple[str, list[Path], tuple[str, ...], str | None]:
144
+ ) -> tuple[str, list[Path], tuple[str, ...], str | None, str | None]:
143
145
  """Run the full compile pipeline for one page.
144
146
 
145
147
  Returns
@@ -148,7 +150,7 @@ def compile_page(
148
150
  ``(xhtml_string, attachment_paths, labels, confluence_status)``
149
151
  """
150
152
  if node.source_path is None:
151
- return "", [], (), None
153
+ return "", [], (), None, None
152
154
 
153
155
  raw = load_page(node)
154
156
 
@@ -190,22 +192,26 @@ def compile_page(
190
192
  site_url = config.page_site_url(node.docs_path or "")
191
193
  if site_url:
192
194
  ir_nodes = attach_source_url(ir_nodes, "", site_url)
195
+ if is_section_index:
196
+ ir_nodes = ir_nodes + (ChildrenMacro(),)
193
197
  if edit_url:
194
198
  abs_path = str(config.docs_dir / (node.docs_path or ""))
195
199
  footer = build_source_footer(edit_url, abs_path)
196
200
  ir_nodes = ir_nodes + (footer,)
197
201
 
198
- # Extract labels and confluence_status from FrontMatter node.
202
+ # Extract labels, confluence_status, and version_message from IR nodes.
199
203
  labels: tuple[str, ...] = ()
200
204
  confluence_status: str | None = None
205
+ version_message: str | None = None
201
206
  for node_item in ir_nodes:
202
207
  if isinstance(node_item, FrontMatter):
203
208
  labels = node_item.labels
204
209
  confluence_status = node_item.confluence_status
205
- break
210
+ if isinstance(node_item, SourceFooter) and node_item.commit_sha and node_item.commit_summary:
211
+ version_message = f"{node_item.commit_sha}: {node_item.commit_summary}"
206
212
 
207
213
  xhtml = emit(ir_nodes)
208
- return xhtml, attachments, labels, confluence_status
214
+ return xhtml, attachments, labels, confluence_status, version_message
209
215
 
210
216
 
211
217
  def _xhtml_hash(xhtml: str) -> str:
@@ -279,8 +285,8 @@ def _plan_nodes(
279
285
  if not quiet:
280
286
  print(f" compiling '{clean_title}' (section index)")
281
287
  try:
282
- xhtml, attachments, labels, confluence_status = compile_page(
283
- index_child, config, link_map, quiet=quiet
288
+ xhtml, attachments, labels, confluence_status, version_message = compile_page(
289
+ index_child, config, link_map, is_section_index=True, quiet=quiet
284
290
  )
285
291
  existing = client.find_page(space_id, clean_title)
286
292
  xhtml_h = _xhtml_hash(xhtml)
@@ -312,6 +318,7 @@ def _plan_nodes(
312
318
  attachments=attachments,
313
319
  labels=labels,
314
320
  confluence_status=confluence_status,
321
+ version_message=version_message,
315
322
  page_id=str(existing["id"]) if existing is not None else None,
316
323
  version=(
317
324
  existing["version"]["number"] if existing is not None else None
@@ -379,7 +386,9 @@ def _plan_nodes(
379
386
  if not quiet:
380
387
  print(f" compiling '{clean_title}'")
381
388
  try:
382
- xhtml, attachments, labels, confluence_status = compile_page(node, config, link_map, quiet=quiet)
389
+ xhtml, attachments, labels, confluence_status, version_message = compile_page(
390
+ node, config, link_map, quiet=quiet
391
+ )
383
392
  except (PageLoadError, OSError) as exc:
384
393
  if not quiet:
385
394
  print(f" skipping '{clean_title}' (error: {exc})")
@@ -419,6 +428,7 @@ def _plan_nodes(
419
428
  attachments=attachments,
420
429
  labels=labels,
421
430
  confluence_status=confluence_status,
431
+ version_message=version_message,
422
432
  page_id=str(existing["id"]) if existing is not None else None,
423
433
  version=(
424
434
  existing["version"]["number"] if existing is not None else None
@@ -588,6 +598,7 @@ def _execute_page_action(
588
598
  action.xhtml or "",
589
599
  action.version + 1,
590
600
  parent_id=action.parent_id,
601
+ version_message=action.version_message,
591
602
  )
592
603
  report.updated += 1
593
604
  except ConfluenceError as upd_exc:
@@ -0,0 +1,107 @@
1
+ """Tests for the ChildrenMacro IR node, emitter, and pipeline integration."""
2
+
3
+ from pathlib import Path
4
+ from unittest.mock import patch
5
+
6
+ from mkdocs_to_confluence.emitter.xhtml import emit
7
+ from mkdocs_to_confluence.ir.nodes import ChildrenMacro
8
+ from mkdocs_to_confluence.publisher.pipeline import compile_page
9
+
10
+ # ── Emitter ───────────────────────────────────────────────────────────────────
11
+
12
+
13
+ def test_children_macro_emits_structured_macro() -> None:
14
+ """ChildrenMacro must emit the Confluence children structured macro."""
15
+ xhtml = emit((ChildrenMacro(),))
16
+ assert 'ac:name="children"' in xhtml
17
+ assert 'ac:parameter ac:name="depth"' in xhtml
18
+ assert ">1<" in xhtml
19
+
20
+
21
+ def test_children_macro_no_extra_params() -> None:
22
+ """Children macro must not emit sort/style params — Confluence defaults are fine."""
23
+ xhtml = emit((ChildrenMacro(),))
24
+ assert "sort" not in xhtml
25
+ assert "style" not in xhtml
26
+
27
+
28
+ # ── Pipeline integration ──────────────────────────────────────────────────────
29
+
30
+
31
+ def test_compile_page_section_index_includes_children_macro(tmp_path: Path) -> None:
32
+ """is_section_index=True must inject ChildrenMacro into emitted XHTML."""
33
+ from mkdocs_to_confluence.loader.config import MkDocsConfig
34
+
35
+ docs = tmp_path / "docs"
36
+ docs.mkdir()
37
+ md = docs / "index.md"
38
+ md.write_text("# Section\n\nIntro text.\n", encoding="utf-8")
39
+
40
+ node = _page_node("Section", md)
41
+ config = MkDocsConfig(site_name="Test", docs_dir=docs, repo_url=None, edit_uri=None, nav=None)
42
+
43
+ xhtml, _, _, _, _ = compile_page(node, config, is_section_index=True)
44
+
45
+ assert 'ac:name="children"' in xhtml
46
+
47
+
48
+ def test_compile_page_non_index_excludes_children_macro(tmp_path: Path) -> None:
49
+ """Regular pages must NOT include the ChildrenMacro."""
50
+ from mkdocs_to_confluence.loader.config import MkDocsConfig
51
+
52
+ docs = tmp_path / "docs"
53
+ docs.mkdir()
54
+ md = docs / "guide.md"
55
+ md.write_text("# Guide\n\nContent.\n", encoding="utf-8")
56
+
57
+ node = _page_node("Guide", md)
58
+ config = MkDocsConfig(site_name="Test", docs_dir=docs, repo_url=None, edit_uri=None, nav=None)
59
+
60
+ xhtml, _, _, _, _ = compile_page(node, config, is_section_index=False)
61
+
62
+ assert 'ac:name="children"' not in xhtml
63
+
64
+
65
+ def test_compile_page_children_macro_before_footer(tmp_path: Path) -> None:
66
+ """ChildrenMacro must appear before the source footer in the XHTML."""
67
+ from mkdocs_to_confluence.loader.config import MkDocsConfig
68
+
69
+ docs = tmp_path / "docs"
70
+ docs.mkdir()
71
+ md = docs / "index.md"
72
+ md.write_text("# Section\n\nIntro.\n", encoding="utf-8")
73
+
74
+ node = _page_node("Section", md)
75
+ config = MkDocsConfig(
76
+ site_name="Test",
77
+ docs_dir=docs,
78
+ repo_url="https://github.com/org/repo",
79
+ edit_uri="edit/main/docs/",
80
+ nav=None,
81
+ )
82
+
83
+ with patch(
84
+ "mkdocs_to_confluence.transforms.footer._last_commit_info", return_value=None
85
+ ):
86
+ xhtml, _, _, _, _ = compile_page(node, config, is_section_index=True)
87
+
88
+ children_pos = xhtml.find('ac:name="children"')
89
+ panel_pos = xhtml.find('ac:name="panel"')
90
+ assert children_pos != -1
91
+ assert panel_pos != -1
92
+ assert children_pos < panel_pos
93
+
94
+
95
+ # ── Helpers ───────────────────────────────────────────────────────────────────
96
+
97
+
98
+ def _page_node(title: str, path: Path) -> object:
99
+ from mkdocs_to_confluence.loader.nav import NavNode
100
+
101
+ return NavNode(
102
+ title=title,
103
+ docs_path=path.name,
104
+ source_path=path,
105
+ level=0,
106
+ children=(),
107
+ )
@@ -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), \
@@ -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}}