mkdocs2confluence 0.12.0__tar.gz → 0.12.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {mkdocs2confluence-0.12.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.12.2}/PKG-INFO +9 -2
  2. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/README.md +7 -1
  3. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/pyproject.toml +2 -1
  4. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2/src/mkdocs2confluence.egg-info}/PKG-INFO +9 -2
  5. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/SOURCES.txt +6 -0
  6. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/requires.txt +1 -0
  7. mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/compiler/__init__.py +6 -0
  8. mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/compiler/models.py +17 -0
  9. mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/compiler/page.py +110 -0
  10. mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/executor.py +378 -0
  11. mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/models.py +60 -0
  12. mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/pipeline.py +46 -0
  13. mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/planner.py +308 -0
  14. mkdocs2confluence-0.12.0/src/mkdocs_to_confluence/publisher/pipeline.py +0 -838
  15. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/LICENSE +0 -0
  16. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/setup.cfg +0 -0
  17. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  18. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  19. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  20. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/__init__.py +0 -0
  21. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/cli.py +0 -0
  22. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  23. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  24. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  25. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/document.py +0 -0
  26. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  27. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  28. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  29. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/config.py +0 -0
  30. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  31. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  32. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/page.py +0 -0
  33. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  34. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  35. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  36. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  37. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  38. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  39. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  40. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  41. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  42. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  43. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  44. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  45. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  46. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/render.py +0 -0
  47. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/server.py +0 -0
  48. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  49. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  50. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/http_retry.py +0 -0
  51. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
  52. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
  53. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/command.py +0 -0
  54. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/comments.py +0 -0
  55. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/github.py +0 -0
  56. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/platform.py +0 -0
  57. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/state.py +0 -0
  58. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  59. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  60. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  61. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  62. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
  63. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  64. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  65. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  66. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_abbrevs.py +0 -0
  67. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_children_macro.py +0 -0
  68. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_cli.py +0 -0
  69. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_editlink.py +0 -0
  70. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_emitter.py +0 -0
  71. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_extra_css.py +0 -0
  72. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_footer.py +0 -0
  73. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_frontmatter.py +0 -0
  74. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_icons.py +0 -0
  75. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_images.py +0 -0
  76. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_internallinks.py +0 -0
  77. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_ir.py +0 -0
  78. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_linkdefs.py +0 -0
  79. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_loader.py +0 -0
  80. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_mermaid.py +0 -0
  81. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_page_loader.py +0 -0
  82. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_parser.py +0 -0
  83. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_pdf.py +0 -0
  84. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_preprocess.py +0 -0
  85. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_preview.py +0 -0
  86. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_publish_client.py +0 -0
  87. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_publish_config.py +0 -0
  88. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_publish_pipeline.py +0 -0
  89. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_server.py +0 -0
  90. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_anchoring.py +0 -0
  91. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_command.py +0 -0
  92. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_comments.py +0 -0
  93. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_github.py +0 -0
  94. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_state.py +0 -0
  95. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.12.0
3
+ Version: 0.12.2
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
@@ -25,6 +25,7 @@ Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: PyYAML>=6.0.3
27
27
  Requires-Dist: httpx>=0.27
28
+ Requires-Dist: idna>=3.15
28
29
  Requires-Dist: tinycss2>=1.5.1
29
30
  Provides-Extra: pdf
30
31
  Requires-Dist: weasyprint>=60.0; extra == "pdf"
@@ -181,7 +182,13 @@ mk2conf publish # go live
181
182
 
182
183
  ![Architecture](https://raw.githubusercontent.com/jeckyl2010/mkdocs2confluence/main/docs/architecture.png)
183
184
 
184
- Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**. The plan phase makes all API read calls; the execute phase makes all write calls in nav order so parent pages always exist before their children.
185
+ Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
186
+
187
+ The publisher is split into two phases:
188
+ - `planner.py` builds a nav-ordered publish plan, compiles pages, and makes the read-side API calls needed to decide create vs update vs skip.
189
+ - `executor.py` applies that plan, performs the write-side API calls, uploads attachments, and wires parent/child relationships in nav order so parent pages always exist before their children.
190
+
191
+ `publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
185
192
 
186
193
  ---
187
194
 
@@ -140,7 +140,13 @@ mk2conf publish # go live
140
140
 
141
141
  ![Architecture](https://raw.githubusercontent.com/jeckyl2010/mkdocs2confluence/main/docs/architecture.png)
142
142
 
143
- Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**. The plan phase makes all API read calls; the execute phase makes all write calls in nav order so parent pages always exist before their children.
143
+ Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
144
+
145
+ The publisher is split into two phases:
146
+ - `planner.py` builds a nav-ordered publish plan, compiles pages, and makes the read-side API calls needed to decide create vs update vs skip.
147
+ - `executor.py` applies that plan, performs the write-side API calls, uploads attachments, and wires parent/child relationships in nav order so parent pages always exist before their children.
148
+
149
+ `publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
144
150
 
145
151
  ---
146
152
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.12.0"
3
+ version = "0.12.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" }
@@ -32,6 +32,7 @@ classifiers = [
32
32
  dependencies = [
33
33
  "PyYAML>=6.0.3",
34
34
  "httpx>=0.27",
35
+ "idna>=3.15",
35
36
  "tinycss2>=1.5.1",
36
37
  ]
37
38
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.12.0
3
+ Version: 0.12.2
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
@@ -25,6 +25,7 @@ Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: PyYAML>=6.0.3
27
27
  Requires-Dist: httpx>=0.27
28
+ Requires-Dist: idna>=3.15
28
29
  Requires-Dist: tinycss2>=1.5.1
29
30
  Provides-Extra: pdf
30
31
  Requires-Dist: weasyprint>=60.0; extra == "pdf"
@@ -181,7 +182,13 @@ mk2conf publish # go live
181
182
 
182
183
  ![Architecture](https://raw.githubusercontent.com/jeckyl2010/mkdocs2confluence/main/docs/architecture.png)
183
184
 
184
- Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**. The plan phase makes all API read calls; the execute phase makes all write calls in nav order so parent pages always exist before their children.
185
+ Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
186
+
187
+ The publisher is split into two phases:
188
+ - `planner.py` builds a nav-ordered publish plan, compiles pages, and makes the read-side API calls needed to decide create vs update vs skip.
189
+ - `executor.py` applies that plan, performs the write-side API calls, uploads attachments, and wires parent/child relationships in nav order so parent pages always exist before their children.
190
+
191
+ `publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
185
192
 
186
193
  ---
187
194
 
@@ -9,6 +9,9 @@ src/mkdocs2confluence.egg-info/requires.txt
9
9
  src/mkdocs2confluence.egg-info/top_level.txt
10
10
  src/mkdocs_to_confluence/__init__.py
11
11
  src/mkdocs_to_confluence/cli.py
12
+ src/mkdocs_to_confluence/compiler/__init__.py
13
+ src/mkdocs_to_confluence/compiler/models.py
14
+ src/mkdocs_to_confluence/compiler/page.py
12
15
  src/mkdocs_to_confluence/emitter/__init__.py
13
16
  src/mkdocs_to_confluence/emitter/xhtml.py
14
17
  src/mkdocs_to_confluence/ir/__init__.py
@@ -37,8 +40,11 @@ src/mkdocs_to_confluence/preview/render.py
37
40
  src/mkdocs_to_confluence/preview/server.py
38
41
  src/mkdocs_to_confluence/publisher/__init__.py
39
42
  src/mkdocs_to_confluence/publisher/client.py
43
+ src/mkdocs_to_confluence/publisher/executor.py
40
44
  src/mkdocs_to_confluence/publisher/http_retry.py
45
+ src/mkdocs_to_confluence/publisher/models.py
41
46
  src/mkdocs_to_confluence/publisher/pipeline.py
47
+ src/mkdocs_to_confluence/publisher/planner.py
42
48
  src/mkdocs_to_confluence/sync/__init__.py
43
49
  src/mkdocs_to_confluence/sync/anchoring.py
44
50
  src/mkdocs_to_confluence/sync/command.py
@@ -1,5 +1,6 @@
1
1
  PyYAML>=6.0.3
2
2
  httpx>=0.27
3
+ idna>=3.15
3
4
  tinycss2>=1.5.1
4
5
 
5
6
  [dev]
@@ -0,0 +1,6 @@
1
+ """Compiler entry points for MkDocs-to-Confluence page compilation."""
2
+
3
+ from mkdocs_to_confluence.compiler.models import CompileResult
4
+ from mkdocs_to_confluence.compiler.page import compile_page
5
+
6
+ __all__ = ["CompileResult", "compile_page"]
@@ -0,0 +1,17 @@
1
+ """Typed models for compiler outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CompileResult:
11
+ """Result of compiling a single MkDocs page to Confluence storage XHTML."""
12
+
13
+ xhtml: str
14
+ attachments: list[Path] = field(default_factory=list)
15
+ labels: tuple[str, ...] = ()
16
+ confluence_status: str | None = None
17
+ version_message: str | None = None
@@ -0,0 +1,110 @@
1
+ """Page compilation pipeline for MkDocs-to-Confluence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from mkdocs_to_confluence.compiler.models import CompileResult
6
+ from mkdocs_to_confluence.emitter.xhtml import emit
7
+ from mkdocs_to_confluence.ir.nodes import ChildrenMacro, FrontMatter, SourceFooter
8
+ from mkdocs_to_confluence.loader.config import MkDocsConfig
9
+ from mkdocs_to_confluence.loader.nav import NavNode
10
+ from mkdocs_to_confluence.loader.page import load_page
11
+ from mkdocs_to_confluence.parser.markdown import parse
12
+ from mkdocs_to_confluence.preprocess.abbrevs import (
13
+ extract_abbreviations,
14
+ strip_abbreviation_defs,
15
+ )
16
+ from mkdocs_to_confluence.preprocess.frontmatter import extract_front_matter
17
+ from mkdocs_to_confluence.preprocess.icons import strip_icon_shortcodes
18
+ from mkdocs_to_confluence.preprocess.includes import (
19
+ preprocess_includes,
20
+ strip_html_comments,
21
+ strip_unsupported_html,
22
+ )
23
+ from mkdocs_to_confluence.preprocess.linkdefs import (
24
+ collect_link_defs,
25
+ expand_link_refs,
26
+ strip_link_defs,
27
+ )
28
+ from mkdocs_to_confluence.transforms.abbrevs import apply_abbreviations
29
+ from mkdocs_to_confluence.transforms.assets import resolve_local_assets
30
+ from mkdocs_to_confluence.transforms.editlink import attach_source_url
31
+ from mkdocs_to_confluence.transforms.footer import build_source_footer
32
+ from mkdocs_to_confluence.transforms.internallinks import resolve_internal_links
33
+ from mkdocs_to_confluence.transforms.mermaid import DEFAULT_KROKI_URL, render_mermaid_diagrams
34
+
35
+
36
+ def compile_page(
37
+ node: NavNode,
38
+ config: MkDocsConfig,
39
+ link_map: dict[str, str] | None = None,
40
+ *,
41
+ is_section_index: bool = False,
42
+ quiet: bool = False,
43
+ ) -> CompileResult:
44
+ """Run the full compile pipeline for one page and return a typed result."""
45
+ if node.source_path is None:
46
+ return CompileResult(xhtml="")
47
+
48
+ raw = load_page(node)
49
+
50
+ preprocessed = preprocess_includes(
51
+ raw,
52
+ source_path=node.source_path,
53
+ docs_dir=config.docs_dir,
54
+ )
55
+ preprocessed = strip_unsupported_html(preprocessed)
56
+ preprocessed = strip_html_comments(preprocessed)
57
+ preprocessed = strip_icon_shortcodes(preprocessed)
58
+ front_matter, preprocessed = extract_front_matter(preprocessed)
59
+ abbrevs = extract_abbreviations(preprocessed)
60
+ preprocessed = strip_abbreviation_defs(preprocessed)
61
+ link_defs = collect_link_defs(preprocessed)
62
+ preprocessed = expand_link_refs(preprocessed, link_defs)
63
+ preprocessed = strip_link_defs(preprocessed)
64
+ ir_nodes = parse(preprocessed)
65
+ if is_section_index:
66
+ ir_nodes = ir_nodes + (ChildrenMacro(),)
67
+ ir_nodes = apply_abbreviations(ir_nodes, abbrevs, page_text=preprocessed)
68
+ ir_nodes, attachments = resolve_local_assets(
69
+ ir_nodes,
70
+ page_path=node.source_path,
71
+ docs_dir=config.docs_dir,
72
+ )
73
+ mermaid_render = config.confluence.mermaid_render if config.confluence else "kroki"
74
+ if mermaid_render != "none":
75
+ kroki_url = (
76
+ mermaid_render[len("kroki:"):] if mermaid_render.startswith("kroki:") else DEFAULT_KROKI_URL
77
+ )
78
+ ir_nodes, mermaid_attachments = render_mermaid_diagrams(ir_nodes, kroki_url, quiet=quiet)
79
+ attachments = attachments + mermaid_attachments
80
+ effective_link_map = link_map if link_map is not None else {}
81
+ if node.docs_path:
82
+ ir_nodes = resolve_internal_links(ir_nodes, effective_link_map, node.docs_path)
83
+ if front_matter is not None:
84
+ ir_nodes = (front_matter,) + ir_nodes
85
+ edit_url = config.page_edit_url(node.docs_path or "")
86
+ site_url = config.page_site_url(node.docs_path or "")
87
+ if site_url:
88
+ ir_nodes = attach_source_url(ir_nodes, "", site_url)
89
+ if edit_url:
90
+ abs_path = str(config.docs_dir / (node.docs_path or ""))
91
+ footer = build_source_footer(edit_url, abs_path)
92
+ ir_nodes = ir_nodes + (footer,)
93
+
94
+ labels: tuple[str, ...] = ()
95
+ confluence_status: str | None = None
96
+ version_message: str | None = None
97
+ for node_item in ir_nodes:
98
+ if isinstance(node_item, FrontMatter):
99
+ labels = node_item.labels
100
+ confluence_status = node_item.confluence_status
101
+ if isinstance(node_item, SourceFooter) and node_item.commit_sha and node_item.commit_summary:
102
+ version_message = f"{node_item.commit_sha}: {node_item.commit_summary}"
103
+
104
+ return CompileResult(
105
+ xhtml=emit(ir_nodes),
106
+ attachments=attachments,
107
+ labels=labels,
108
+ confluence_status=confluence_status,
109
+ version_message=version_message,
110
+ )
@@ -0,0 +1,378 @@
1
+ """Execution helpers for the nav-driven publish pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from mkdocs_to_confluence.publisher.client import ConfluenceError
11
+ from mkdocs_to_confluence.publisher.models import PageAction, PublishReport
12
+ from mkdocs_to_confluence.transforms.assets import _make_attachment_name
13
+
14
+ if TYPE_CHECKING:
15
+ from mkdocs_to_confluence.publisher.client import ConfluenceClient
16
+
17
+
18
+ def _upload_assets(
19
+ page_id: str,
20
+ attachments: list[Path],
21
+ docs_dir: Path,
22
+ client: ConfluenceClient,
23
+ *,
24
+ quiet: bool = False,
25
+ ) -> tuple[int, int, list[tuple[str, str]]]:
26
+ """Upload attachments for one page sequentially."""
27
+ pairs = [(_make_attachment_name(p, docs_dir), p) for p in attachments]
28
+ uploaded = 0
29
+ skipped = 0
30
+ errors: list[tuple[str, str]] = []
31
+
32
+ # Fetch once; reuse across all uploads (read-only).
33
+ existing = client.list_attachments(page_id)
34
+
35
+ for name, path in pairs:
36
+ # Skip upload if local file is not newer than what Confluence already has.
37
+ if name in existing:
38
+ try:
39
+ created_at = existing[name]["version"]["createdAt"]
40
+ confluence_ts = datetime.fromisoformat(
41
+ created_at.replace("Z", "+00:00")
42
+ )
43
+ local_mtime = datetime.fromtimestamp(
44
+ path.stat().st_mtime, tz=timezone.utc
45
+ )
46
+ if local_mtime <= confluence_ts:
47
+ if not quiet:
48
+ print(f" skipping {name} (unchanged)")
49
+ skipped += 1
50
+ continue
51
+ except (KeyError, ValueError, OSError):
52
+ pass # can't compare — fall through to upload
53
+
54
+ if not quiet:
55
+ print(f" uploading {name}")
56
+ try:
57
+ client.upload_attachment(page_id, path, name, existing)
58
+ uploaded += 1
59
+ except Exception as exc:
60
+ errors.append((name, str(exc)))
61
+
62
+ return uploaded, skipped, errors
63
+
64
+
65
+ def _execute_folder_action(
66
+ action: PageAction,
67
+ client: ConfluenceClient,
68
+ space_id: str,
69
+ root_page_id: str | None,
70
+ report: PublishReport,
71
+ *,
72
+ quiet: bool = False,
73
+ ) -> None:
74
+ """Handle folder create/find for a single folder action."""
75
+ if action.page_id is not None:
76
+ # Already found at plan time — reuse.
77
+ report.updated += 1
78
+ elif action.parent_is_folder or action.parent_id == root_page_id:
79
+ # Parent is a Confluence folder, or this is a top-level section
80
+ # directly under the configured root page — use native folder API.
81
+ existing_folder = None
82
+ if action.parent_id is not None:
83
+ try:
84
+ existing_folder = client.find_folder_under(
85
+ action.parent_id,
86
+ action.title,
87
+ parent_is_folder=action.parent_is_folder,
88
+ )
89
+ except Exception as find_exc:
90
+ print(
91
+ f" [warn] find_folder_under failed "
92
+ f"(parent_id={action.parent_id}): {find_exc}",
93
+ file=sys.stderr,
94
+ )
95
+ if existing_folder is not None:
96
+ action.page_id = str(existing_folder["id"])
97
+ report.updated += 1
98
+ else:
99
+ folder_parent = (
100
+ action.parent_id if action.parent_is_folder else None
101
+ )
102
+ folder = client.create_folder(
103
+ space_id, action.title, parent_id=folder_parent
104
+ )
105
+ action.page_id = str(folder["id"])
106
+ report.created += 1
107
+ if not quiet:
108
+ print(
109
+ f" folder id={action.page_id}"
110
+ f" parent_id={action.parent_id}"
111
+ f" parent_is_folder={action.parent_is_folder}"
112
+ )
113
+ else:
114
+ # Parent is a dynamically-created page (e.g. a section-index page).
115
+ # Confluence folders cannot be nested under pages — use a stub page.
116
+ action.is_folder = False
117
+ existing = client.find_page(space_id, action.title)
118
+ if existing is not None:
119
+ action.page_id = str(existing["id"])
120
+ report.updated += 1
121
+ else:
122
+ stub = client.create_page(
123
+ space_id, action.title, "", parent_id=action.parent_id,
124
+ )
125
+ action.page_id = str(stub["id"])
126
+ report.created += 1
127
+
128
+
129
+ def _execute_page_action(
130
+ action: PageAction,
131
+ client: ConfluenceClient,
132
+ space_id: str,
133
+ report: PublishReport,
134
+ ) -> None:
135
+ """Handle create/update (with stale fallback) for a single page action."""
136
+ if action.action == "create":
137
+ page = client.create_page(
138
+ space_id,
139
+ action.title,
140
+ action.xhtml or "",
141
+ parent_id=action.parent_id,
142
+ )
143
+ action.page_id = str(page["id"])
144
+ report.created += 1
145
+ try:
146
+ client.stamp_managed(action.page_id)
147
+ except Exception:
148
+ pass # non-fatal
149
+ elif action.action == "update":
150
+ if action.page_id is None or action.version is None:
151
+ raise RuntimeError(
152
+ f"Update action for '{action.title}' is missing page_id or version"
153
+ )
154
+ try:
155
+ client.update_page(
156
+ action.page_id,
157
+ action.title,
158
+ action.xhtml or "",
159
+ action.version + 1,
160
+ parent_id=action.parent_id,
161
+ version_message=action.version_message,
162
+ )
163
+ report.updated += 1
164
+ except ConfluenceError as upd_exc:
165
+ err = str(upd_exc)
166
+ # 404 = page deleted; 400 "another space" = stale page_id
167
+ # from a different Confluence space. Both mean the existing
168
+ # page can't be updated — fall back to create a fresh one.
169
+ is_stale = "HTTP 404" in err or (
170
+ "HTTP 400" in err and "another space" in err.lower()
171
+ )
172
+ if not is_stale:
173
+ raise
174
+ print(
175
+ f" [warn] update failed ({err[:80].strip()}) —"
176
+ " stale page_id; falling back to create",
177
+ file=sys.stderr,
178
+ )
179
+ action.page_id = None
180
+ page = client.create_page(
181
+ space_id,
182
+ action.title,
183
+ action.xhtml or "",
184
+ parent_id=action.parent_id,
185
+ )
186
+ action.page_id = str(page["id"])
187
+ report.created += 1
188
+ try:
189
+ client.stamp_managed(action.page_id)
190
+ except Exception:
191
+ pass # non-fatal
192
+
193
+
194
+ def _wire_children(
195
+ action: PageAction,
196
+ action_by_node: dict[int, PageAction],
197
+ ) -> None:
198
+ """Propagate a section's resolved page_id to all its direct children."""
199
+ if action.page_id is None:
200
+ return
201
+ for child_node in action.node.children:
202
+ child_action = action_by_node.get(id(child_node))
203
+ if child_action is not None:
204
+ child_action.parent_id = action.page_id
205
+ child_action.parent_is_folder = action.is_folder
206
+
207
+
208
+ def _apply_page_presentation(
209
+ action: PageAction,
210
+ client: ConfluenceClient,
211
+ *,
212
+ full_width: bool,
213
+ space_key: str | None = None,
214
+ suppress_full_width_errors: bool = False,
215
+ ) -> None:
216
+ """Apply page status and full-width presentation updates."""
217
+ if action.page_id and action.confluence_status and not action.is_folder:
218
+ try:
219
+ print(f" [status] setting '{action.confluence_status}' on page {action.page_id!r}...")
220
+ client.set_page_status(action.page_id, action.confluence_status, space_key=space_key)
221
+ print(" [status] ok")
222
+ except Exception as exc:
223
+ # Always print status errors — user configured status explicitly
224
+ print(f" [warn] could not set page status '{action.confluence_status}': {exc}")
225
+
226
+ # Set full-width LAST — Confluence's state/label PUTs can reset the appearance property.
227
+ if full_width and action.page_id and not action.is_folder:
228
+ try:
229
+ client.set_page_full_width(action.page_id)
230
+ except Exception as exc:
231
+ if not suppress_full_width_errors:
232
+ print(f" [warn] could not set full-width on page {action.page_id!r}: {exc}")
233
+
234
+
235
+ def _post_process_action(
236
+ action: PageAction,
237
+ client: ConfluenceClient,
238
+ *,
239
+ full_width: bool,
240
+ docs_dir: Path,
241
+ report: PublishReport,
242
+ space_key: str | None = None,
243
+ quiet: bool = False,
244
+ ) -> None:
245
+ """Run all non-fatal post-create/update work for a single action."""
246
+ # Store content hash after create/update so the next run can skip unchanged pages.
247
+ if action.page_id and action.content_hash and action.action in ("create", "update"):
248
+ try:
249
+ client.set_content_hash(action.page_id, action.content_hash)
250
+ except Exception:
251
+ pass # non-fatal
252
+
253
+ # Apply labels (tags) from front matter — non-fatal on failure.
254
+ if action.page_id and action.labels and not action.is_folder:
255
+ try:
256
+ client.set_page_labels(action.page_id, action.labels)
257
+ except Exception:
258
+ pass
259
+
260
+ _apply_page_presentation(
261
+ action,
262
+ client,
263
+ full_width=full_width,
264
+ space_key=space_key,
265
+ suppress_full_width_errors=True,
266
+ )
267
+
268
+ # Upload assets — skip files whose mtime is not newer than Confluence.
269
+ if action.page_id and action.attachments:
270
+ uploaded, asset_skipped, asset_errors = _upload_assets(
271
+ action.page_id, action.attachments, docs_dir, client, quiet=quiet
272
+ )
273
+ report.assets_uploaded += uploaded
274
+ report.assets_skipped += asset_skipped
275
+ for name, msg in asset_errors:
276
+ report.errors.append((f"{action.title} / {name}", msg))
277
+
278
+
279
+ def execute_publish(
280
+ plan: list[PageAction],
281
+ client: ConfluenceClient,
282
+ *,
283
+ dry_run: bool = False,
284
+ space_id: str,
285
+ space_key: str | None = None,
286
+ docs_dir: Path,
287
+ full_width: bool = True,
288
+ root_page_id: str | None = None,
289
+ prune: bool = False,
290
+ quiet: bool = False,
291
+ ) -> PublishReport:
292
+ """Execute the publish plan."""
293
+ report = PublishReport()
294
+
295
+ if dry_run:
296
+ report.skipped = sum(1 for a in plan if a.action == "skip")
297
+ report.created = sum(1 for a in plan if a.action == "create")
298
+ report.updated = sum(1 for a in plan if a.action == "update")
299
+ return report
300
+
301
+ # Index: nav node id → PageAction, used to wire child parent_ids after
302
+ # each section is created/updated.
303
+ action_by_node: dict[int, PageAction] = {id(a.node): a for a in plan}
304
+
305
+ active = [a for a in plan if a.action != "skip"]
306
+ total = len(active)
307
+ if not quiet:
308
+ print(f"\nPublishing {total} page(s)...")
309
+ counter = 0
310
+
311
+ for action in plan:
312
+ if action.action == "skip":
313
+ report.skipped += 1
314
+ _apply_page_presentation(action, client, full_width=full_width, space_key=space_key)
315
+ continue
316
+
317
+ counter += 1
318
+ if not quiet:
319
+ print(f" [{counter}/{total}] {action.action:<6} '{action.title}'")
320
+
321
+ try:
322
+ if action.is_folder:
323
+ _execute_folder_action(action, client, space_id, root_page_id, report, quiet=quiet)
324
+ else:
325
+ _execute_page_action(action, client, space_id, report)
326
+ except Exception as exc:
327
+ report.errors.append((action.title, str(exc)))
328
+ # Do NOT `continue` — all post-execute blocks below are guarded by
329
+ # action.page_id checks, so they are safely skipped on failure.
330
+ # Critically, child parent_id wiring must still run for section
331
+ # pages whose children were planned with parent_id=None.
332
+
333
+ if action.node.is_section and action.page_id:
334
+ _wire_children(action, action_by_node)
335
+
336
+ _post_process_action(
337
+ action, client,
338
+ full_width=full_width, docs_dir=docs_dir, space_key=space_key, report=report, quiet=quiet,
339
+ )
340
+
341
+ if prune and root_page_id:
342
+ published_ids = {a.page_id for a in plan if a.page_id}
343
+ _prune_orphans(client, root_page_id, published_ids, report, quiet=quiet)
344
+
345
+ return report
346
+
347
+
348
+ def _prune_orphans(
349
+ client: ConfluenceClient,
350
+ root_page_id: str,
351
+ published_ids: set[str],
352
+ report: PublishReport,
353
+ *,
354
+ quiet: bool = False,
355
+ ) -> None:
356
+ """Delete managed descendant pages that are no longer in the publish plan."""
357
+ try:
358
+ all_descendants = client.get_descendant_ids(root_page_id)
359
+ except Exception as exc:
360
+ print(f" [warn] prune: could not fetch descendants — {exc}", file=sys.stderr)
361
+ return
362
+
363
+ orphan_candidates = [pid for pid in all_descendants if pid not in published_ids]
364
+ if not orphan_candidates:
365
+ return
366
+
367
+ if not quiet:
368
+ print(f"\nPruning: checking {len(orphan_candidates)} orphan candidate(s)...")
369
+ for page_id in orphan_candidates:
370
+ try:
371
+ if not client.is_managed(page_id):
372
+ continue
373
+ client.delete_page(page_id)
374
+ report.pruned += 1
375
+ if not quiet:
376
+ print(f" deleted orphan page {page_id}")
377
+ except Exception as exc:
378
+ print(f" [warn] prune: failed to delete page {page_id} — {exc}", file=sys.stderr)