mkdocs2confluence 0.12.0__tar.gz → 0.13.0__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 (101) hide show
  1. {mkdocs2confluence-0.12.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.13.0}/PKG-INFO +25 -2
  2. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/README.md +23 -1
  3. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/pyproject.toml +5 -1
  4. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0/src/mkdocs2confluence.egg-info}/PKG-INFO +25 -2
  5. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs2confluence.egg-info/SOURCES.txt +12 -0
  6. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs2confluence.egg-info/requires.txt +1 -0
  7. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/cli.py +68 -3
  8. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/compiler/__init__.py +6 -0
  9. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/compiler/models.py +17 -0
  10. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/compiler/page.py +110 -0
  11. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/loader/config.py +18 -0
  12. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/publisher/changelog.py +131 -0
  13. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/publisher/executor.py +378 -0
  14. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/publisher/models.py +60 -0
  15. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/publisher/pipeline.py +46 -0
  16. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/publisher/planner.py +308 -0
  17. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/skill_installer.py +99 -0
  18. mkdocs2confluence-0.13.0/src/mkdocs_to_confluence/skills/mkdocs-changelog/SKILL.md +78 -0
  19. mkdocs2confluence-0.13.0/tests/test_changelog_config.py +62 -0
  20. mkdocs2confluence-0.13.0/tests/test_changelog_publish.py +194 -0
  21. mkdocs2confluence-0.13.0/tests/test_skill_installer.py +126 -0
  22. mkdocs2confluence-0.12.0/src/mkdocs_to_confluence/publisher/pipeline.py +0 -838
  23. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/LICENSE +0 -0
  24. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/setup.cfg +0 -0
  25. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  26. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  27. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  28. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/__init__.py +0 -0
  29. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  30. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  31. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  32. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/ir/document.py +0 -0
  33. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  34. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  35. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  36. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  37. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  38. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/loader/page.py +0 -0
  39. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  40. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  41. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  42. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  43. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  44. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  45. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  46. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  47. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  48. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  49. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  50. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  51. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  52. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preview/render.py +0 -0
  53. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/preview/server.py +0 -0
  54. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  55. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  56. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/publisher/http_retry.py +0 -0
  57. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
  58. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
  59. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/sync/command.py +0 -0
  60. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/sync/comments.py +0 -0
  61. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/sync/github.py +0 -0
  62. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/sync/platform.py +0 -0
  63. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/sync/state.py +0 -0
  64. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  65. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  66. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  67. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  68. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
  69. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  70. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  71. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  72. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_abbrevs.py +0 -0
  73. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_children_macro.py +0 -0
  74. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_cli.py +0 -0
  75. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_editlink.py +0 -0
  76. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_emitter.py +0 -0
  77. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_extra_css.py +0 -0
  78. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_footer.py +0 -0
  79. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_frontmatter.py +0 -0
  80. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_icons.py +0 -0
  81. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_images.py +0 -0
  82. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_internallinks.py +0 -0
  83. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_ir.py +0 -0
  84. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_linkdefs.py +0 -0
  85. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_loader.py +0 -0
  86. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_mermaid.py +0 -0
  87. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_page_loader.py +0 -0
  88. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_parser.py +0 -0
  89. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_pdf.py +0 -0
  90. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_preprocess.py +0 -0
  91. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_preview.py +0 -0
  92. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_publish_client.py +0 -0
  93. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_publish_config.py +0 -0
  94. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_publish_pipeline.py +0 -0
  95. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_server.py +0 -0
  96. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_sync_anchoring.py +0 -0
  97. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_sync_command.py +0 -0
  98. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_sync_comments.py +0 -0
  99. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_sync_github.py +0 -0
  100. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/tests/test_sync_state.py +0 -0
  101. {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.13.0}/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.13.0
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"
@@ -152,10 +153,26 @@ confluence:
152
153
  parent_page_id: "123456" # optional root page
153
154
  mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
154
155
  full_width: true # default: true
156
+ changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
155
157
  ```
156
158
 
157
159
  The `confluence:` block is also accepted under `extra:` for MkDocs strict-mode compatibility. The API token is read from `token:` in `mkdocs.yml`, then `CONFLUENCE_API_TOKEN`, then `MK2CONF_TOKEN`.
158
160
 
161
+ ### Changelog / What's New page
162
+
163
+ Set `changelog:` to a Markdown file path (relative to `docs_dir`) to have mk2conf publish it as a permanent top-level page on every full `mk2conf publish` run. The page title comes from YAML front matter `title:`; it defaults to `"What's New"` if absent.
164
+
165
+ ```yaml
166
+ confluence:
167
+ changelog: CHANGELOG.md # relative to docs_dir
168
+ ```
169
+
170
+ - The page does **not** need to appear in `nav:` — it is always placed at the top level of the space (or under `parent_page_id` if set).
171
+ - If it also appears in `nav:`, it is published once; no duplication.
172
+ - `--prune` never deletes it — it is a pinned page, not a nav-derived page.
173
+ - Partial runs (`--page` / `--section`) skip the changelog page, consistent with other publish behaviour.
174
+ - Omit the key, or set it to an empty string, to disable the feature entirely.
175
+
159
176
  **Your first publish:**
160
177
 
161
178
  ```bash
@@ -181,7 +198,13 @@ mk2conf publish # go live
181
198
 
182
199
  ![Architecture](https://raw.githubusercontent.com/jeckyl2010/mkdocs2confluence/main/docs/architecture.png)
183
200
 
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.
201
+ Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
202
+
203
+ The publisher is split into two phases:
204
+ - `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.
205
+ - `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.
206
+
207
+ `publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
185
208
 
186
209
  ---
187
210
 
@@ -111,10 +111,26 @@ confluence:
111
111
  parent_page_id: "123456" # optional root page
112
112
  mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
113
113
  full_width: true # default: true
114
+ changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
114
115
  ```
115
116
 
116
117
  The `confluence:` block is also accepted under `extra:` for MkDocs strict-mode compatibility. The API token is read from `token:` in `mkdocs.yml`, then `CONFLUENCE_API_TOKEN`, then `MK2CONF_TOKEN`.
117
118
 
119
+ ### Changelog / What's New page
120
+
121
+ Set `changelog:` to a Markdown file path (relative to `docs_dir`) to have mk2conf publish it as a permanent top-level page on every full `mk2conf publish` run. The page title comes from YAML front matter `title:`; it defaults to `"What's New"` if absent.
122
+
123
+ ```yaml
124
+ confluence:
125
+ changelog: CHANGELOG.md # relative to docs_dir
126
+ ```
127
+
128
+ - The page does **not** need to appear in `nav:` — it is always placed at the top level of the space (or under `parent_page_id` if set).
129
+ - If it also appears in `nav:`, it is published once; no duplication.
130
+ - `--prune` never deletes it — it is a pinned page, not a nav-derived page.
131
+ - Partial runs (`--page` / `--section`) skip the changelog page, consistent with other publish behaviour.
132
+ - Omit the key, or set it to an empty string, to disable the feature entirely.
133
+
118
134
  **Your first publish:**
119
135
 
120
136
  ```bash
@@ -140,7 +156,13 @@ mk2conf publish # go live
140
156
 
141
157
  ![Architecture](https://raw.githubusercontent.com/jeckyl2010/mkdocs2confluence/main/docs/architecture.png)
142
158
 
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.
159
+ Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
160
+
161
+ The publisher is split into two phases:
162
+ - `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.
163
+ - `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.
164
+
165
+ `publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
144
166
 
145
167
  ---
146
168
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.12.0"
3
+ version = "0.13.0"
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
 
@@ -66,6 +67,9 @@ build-backend = "setuptools.build_meta"
66
67
  [tool.setuptools.packages.find]
67
68
  where = ["src"]
68
69
 
70
+ [tool.setuptools.package-data]
71
+ "mkdocs_to_confluence" = ["skills/**/*.md"]
72
+
69
73
  [tool.ruff]
70
74
  line-length = 120
71
75
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.12.0
3
+ Version: 0.13.0
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"
@@ -152,10 +153,26 @@ confluence:
152
153
  parent_page_id: "123456" # optional root page
153
154
  mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
154
155
  full_width: true # default: true
156
+ changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
155
157
  ```
156
158
 
157
159
  The `confluence:` block is also accepted under `extra:` for MkDocs strict-mode compatibility. The API token is read from `token:` in `mkdocs.yml`, then `CONFLUENCE_API_TOKEN`, then `MK2CONF_TOKEN`.
158
160
 
161
+ ### Changelog / What's New page
162
+
163
+ Set `changelog:` to a Markdown file path (relative to `docs_dir`) to have mk2conf publish it as a permanent top-level page on every full `mk2conf publish` run. The page title comes from YAML front matter `title:`; it defaults to `"What's New"` if absent.
164
+
165
+ ```yaml
166
+ confluence:
167
+ changelog: CHANGELOG.md # relative to docs_dir
168
+ ```
169
+
170
+ - The page does **not** need to appear in `nav:` — it is always placed at the top level of the space (or under `parent_page_id` if set).
171
+ - If it also appears in `nav:`, it is published once; no duplication.
172
+ - `--prune` never deletes it — it is a pinned page, not a nav-derived page.
173
+ - Partial runs (`--page` / `--section`) skip the changelog page, consistent with other publish behaviour.
174
+ - Omit the key, or set it to an empty string, to disable the feature entirely.
175
+
159
176
  **Your first publish:**
160
177
 
161
178
  ```bash
@@ -181,7 +198,13 @@ mk2conf publish # go live
181
198
 
182
199
  ![Architecture](https://raw.githubusercontent.com/jeckyl2010/mkdocs2confluence/main/docs/architecture.png)
183
200
 
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.
201
+ Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
202
+
203
+ The publisher is split into two phases:
204
+ - `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.
205
+ - `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.
206
+
207
+ `publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
185
208
 
186
209
  ---
187
210
 
@@ -9,6 +9,10 @@ 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/skill_installer.py
13
+ src/mkdocs_to_confluence/compiler/__init__.py
14
+ src/mkdocs_to_confluence/compiler/models.py
15
+ src/mkdocs_to_confluence/compiler/page.py
12
16
  src/mkdocs_to_confluence/emitter/__init__.py
13
17
  src/mkdocs_to_confluence/emitter/xhtml.py
14
18
  src/mkdocs_to_confluence/ir/__init__.py
@@ -36,9 +40,14 @@ src/mkdocs_to_confluence/preview/__init__.py
36
40
  src/mkdocs_to_confluence/preview/render.py
37
41
  src/mkdocs_to_confluence/preview/server.py
38
42
  src/mkdocs_to_confluence/publisher/__init__.py
43
+ src/mkdocs_to_confluence/publisher/changelog.py
39
44
  src/mkdocs_to_confluence/publisher/client.py
45
+ src/mkdocs_to_confluence/publisher/executor.py
40
46
  src/mkdocs_to_confluence/publisher/http_retry.py
47
+ src/mkdocs_to_confluence/publisher/models.py
41
48
  src/mkdocs_to_confluence/publisher/pipeline.py
49
+ src/mkdocs_to_confluence/publisher/planner.py
50
+ src/mkdocs_to_confluence/skills/mkdocs-changelog/SKILL.md
42
51
  src/mkdocs_to_confluence/sync/__init__.py
43
52
  src/mkdocs_to_confluence/sync/anchoring.py
44
53
  src/mkdocs_to_confluence/sync/command.py
@@ -55,6 +64,8 @@ src/mkdocs_to_confluence/transforms/images.py
55
64
  src/mkdocs_to_confluence/transforms/internallinks.py
56
65
  src/mkdocs_to_confluence/transforms/mermaid.py
57
66
  tests/test_abbrevs.py
67
+ tests/test_changelog_config.py
68
+ tests/test_changelog_publish.py
58
69
  tests/test_children_macro.py
59
70
  tests/test_cli.py
60
71
  tests/test_editlink.py
@@ -78,6 +89,7 @@ tests/test_publish_client.py
78
89
  tests/test_publish_config.py
79
90
  tests/test_publish_pipeline.py
80
91
  tests/test_server.py
92
+ tests/test_skill_installer.py
81
93
  tests/test_sync_anchoring.py
82
94
  tests/test_sync_command.py
83
95
  tests/test_sync_comments.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]
@@ -265,6 +265,33 @@ def _build_parser() -> argparse.ArgumentParser:
265
265
  help="Suppress progress output.",
266
266
  )
267
267
 
268
+ # --- install-skill ---
269
+ is_ = sub.add_parser(
270
+ "install-skill",
271
+ help="Install the mkdocs-changelog AI skill into detected tool directories.",
272
+ formatter_class=argparse.RawDescriptionHelpFormatter,
273
+ epilog=(
274
+ "Detected targets (all installed when no --tool given):\n"
275
+ " hermes ~/.hermes/skills/tooling/mkdocs-changelog/SKILL.md\n"
276
+ " github-skills .github/skills/tooling/mkdocs-changelog/SKILL.md\n"
277
+ " claude .claude/commands/changelog.md (frontmatter stripped)\n"
278
+ " copilot .github/instructions/mk2conf-changelog.instructions.md\n"
279
+ " cursor .cursor/rules/mk2conf-changelog.mdc\n"
280
+ "\n"
281
+ "Examples:\n"
282
+ " mk2conf install-skill\n"
283
+ " mk2conf install-skill --tool claude\n"
284
+ " mk2conf install-skill --tool hermes\n"
285
+ ),
286
+ )
287
+ is_.add_argument(
288
+ "--tool",
289
+ metavar="NAME",
290
+ default=None,
291
+ choices=["hermes", "github-skills", "claude", "copilot", "cursor"],
292
+ help="Install only to a specific tool (hermes, github-skills, claude, copilot, cursor).",
293
+ )
294
+
268
295
  return parser
269
296
 
270
297
 
@@ -288,6 +315,8 @@ def main(argv: list[str] | None = None) -> None:
288
315
  _cmd_pdf(args)
289
316
  elif args.command == "sync-comments":
290
317
  _cmd_sync_comments(args)
318
+ elif args.command == "install-skill":
319
+ _cmd_install_skill(args)
291
320
  except (ValueError, FileNotFoundError) as exc:
292
321
  print(f"error: {exc}", file=sys.stderr)
293
322
  sys.exit(1)
@@ -499,6 +528,8 @@ def _cmd_publish(args: argparse.Namespace) -> None:
499
528
  sys.exit(1)
500
529
  nav_nodes = [node]
501
530
 
531
+ partial = bool(getattr(args, "page", None) or getattr(args, "section", None))
532
+
502
533
  if args.dry_run:
503
534
  # When a section is given, show what node was matched so the user can
504
535
  # verify the section resolved correctly (section vs. leaf page).
@@ -513,8 +544,14 @@ def _cmd_publish(args: argparse.Namespace) -> None:
513
544
  print(f"Dry run: would publish {len(pages)} page(s) to {conf_config.base_url}")
514
545
  for page in pages:
515
546
  print(f" {page.docs_path} → '{page.title}'")
547
+ if conf_config.changelog_file and not partial:
548
+ from mkdocs_to_confluence.publisher.changelog import _extract_title
549
+ cl_path = config.docs_dir / conf_config.changelog_file
550
+ cl_title = _extract_title(cl_path) or "What's New"
551
+ print(f" {conf_config.changelog_file} → '{cl_title}' (changelog, top-level)")
516
552
  return
517
553
 
554
+ from mkdocs_to_confluence.publisher.changelog import publish_changelog
518
555
  from mkdocs_to_confluence.publisher.client import ConfluenceClient, ConfluenceError
519
556
  from mkdocs_to_confluence.publisher.pipeline import execute_publish, plan_publish
520
557
 
@@ -535,9 +572,6 @@ def _cmd_publish(args: argparse.Namespace) -> None:
535
572
  nav_nodes, client, config, conf_config,
536
573
  space_id=space_id, quiet=args.quiet, full_nav_nodes=all_nav_nodes,
537
574
  )
538
- # --prune is silently disabled for partial publishes (--page / --section)
539
- # because published_ids would only cover the subset, not the full nav.
540
- partial = bool(getattr(args, "page", None) or getattr(args, "section", None))
541
575
  report = execute_publish(
542
576
  plan, client, dry_run=False, space_id=space_id,
543
577
  space_key=conf_config.space_key,
@@ -546,6 +580,13 @@ def _cmd_publish(args: argparse.Namespace) -> None:
546
580
  prune=getattr(args, "prune", False) and not partial,
547
581
  quiet=args.quiet,
548
582
  )
583
+ # Changelog is a pinned top-level page — always publish on full runs,
584
+ # skip on partial runs (--page / --section) like all other publish behaviour.
585
+ if not partial:
586
+ publish_changelog(
587
+ config, conf_config, client, space_id,
588
+ space_key=conf_config.space_key, quiet=args.quiet,
589
+ )
549
590
  except ConfluenceError as exc:
550
591
  print(f"error: {exc}", file=sys.stderr)
551
592
  sys.exit(1)
@@ -599,6 +640,30 @@ def _cmd_publish(args: argparse.Namespace) -> None:
599
640
  sys.exit(1)
600
641
 
601
642
 
643
+ def _cmd_install_skill(args: argparse.Namespace) -> None:
644
+ from mkdocs_to_confluence.skill_installer import install_skill
645
+
646
+ installed = install_skill(tool=getattr(args, "tool", None))
647
+
648
+ if not installed:
649
+ print(
650
+ "No AI tool directories detected and no --tool specified.\n"
651
+ "Run with --tool to install to a specific tool, e.g.:\n"
652
+ " mk2conf install-skill --tool claude",
653
+ file=sys.stderr,
654
+ )
655
+ sys.exit(1)
656
+
657
+ for tool_name, dest in installed:
658
+ print(f" installed [{tool_name}] {dest}")
659
+
660
+ if any(name == "fallback" for name, _ in installed):
661
+ print(
662
+ "\nNo AI tool directories were detected. Skill written to .mk2conf/changelog-skill.md.\n"
663
+ "Copy its contents to your AI tool's custom instructions or skill directory.",
664
+ )
665
+
666
+
602
667
  def _cmd_pdf(args: argparse.Namespace) -> None:
603
668
  # On macOS with uv/non-system Python, Homebrew libs are not on the dyld search
604
669
  # path. Re-exec once with DYLD_LIBRARY_PATH set — the sentinel prevents loops.
@@ -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
+ )
@@ -34,6 +34,7 @@ class ConfluenceConfig:
34
34
  github_token: str | None = None # GitHub PAT (falls back to GITHUB_TOKEN env var)
35
35
  github_base_branch: str = "main" # base branch for review PRs
36
36
  allow_any_host: bool = False # set True to allow non-Atlassian Cloud base_url hosts
37
+ changelog_file: str | None = None # path relative to docs_dir; None means disabled
37
38
 
38
39
 
39
40
  @dataclass(frozen=True)
@@ -259,6 +260,22 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
259
260
  if not token:
260
261
  token = os.environ.get("MK2CONF_TOKEN", "")
261
262
 
263
+ # changelog (optional) — path relative to docs_dir
264
+ changelog_file: str | None = None
265
+ raw_changelog = raw_conf.get("changelog")
266
+ if raw_changelog is not None:
267
+ cl_str = str(raw_changelog).strip()
268
+ if cl_str:
269
+ candidate = (docs_dir / cl_str).resolve()
270
+ try:
271
+ candidate.relative_to(docs_dir)
272
+ except ValueError:
273
+ raise ConfigError(
274
+ f"mkdocs.yml: 'confluence.changelog' path {cl_str!r} "
275
+ "escapes docs_dir. The path must be relative to the docs directory."
276
+ )
277
+ changelog_file = cl_str
278
+
262
279
  confluence = ConfluenceConfig(
263
280
  base_url=base_url.rstrip("/"),
264
281
  space_key=space_key,
@@ -273,6 +290,7 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
273
290
  else os.environ.get("GITHUB_TOKEN") or None),
274
291
  github_base_branch=str(raw_conf.get("github_base_branch", "main")),
275
292
  allow_any_host=allow_any_host,
293
+ changelog_file=changelog_file,
276
294
  )
277
295
 
278
296
  # --- extra_css (optional) ---
@@ -0,0 +1,131 @@
1
+ """Compile and publish the standalone changelog page."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ import yaml
11
+
12
+ from mkdocs_to_confluence.loader.nav import NavNode
13
+ from mkdocs_to_confluence.publisher.executor import _upload_assets
14
+ from mkdocs_to_confluence.publisher.planner import _xhtml_hash, compile_page
15
+
16
+ if TYPE_CHECKING:
17
+ from mkdocs_to_confluence.loader.config import ConfluenceConfig, MkDocsConfig
18
+ from mkdocs_to_confluence.publisher.client import ConfluenceClient
19
+
20
+ _FRONT_MATTER_RE = re.compile(r"\A---\s*\n(.*?\n?)---\s*\n?", re.DOTALL)
21
+
22
+
23
+ def _extract_title(source_path: Path) -> str | None:
24
+ """Return the ``title`` value from YAML front matter, or ``None`` if absent."""
25
+ try:
26
+ raw = source_path.read_text(encoding="utf-8")
27
+ except OSError:
28
+ return None
29
+ m = _FRONT_MATTER_RE.match(raw)
30
+ if not m:
31
+ return None
32
+ try:
33
+ fm: object = yaml.safe_load(m.group(1))
34
+ except yaml.YAMLError:
35
+ return None
36
+ if not isinstance(fm, dict):
37
+ return None
38
+ val = fm.get("title")
39
+ return str(val).strip() if val else None
40
+
41
+
42
+ def publish_changelog(
43
+ config: MkDocsConfig,
44
+ conf_config: ConfluenceConfig,
45
+ client: ConfluenceClient,
46
+ space_id: str,
47
+ *,
48
+ space_key: str | None = None,
49
+ quiet: bool = False,
50
+ ) -> None:
51
+ """Compile and publish the changelog page if ``conf_config.changelog_file`` is set."""
52
+ if not conf_config.changelog_file:
53
+ return
54
+
55
+ changelog_path = config.docs_dir / conf_config.changelog_file
56
+ if not changelog_path.exists():
57
+ print(
58
+ f" [warn] changelog: file not found: {changelog_path}",
59
+ file=sys.stderr,
60
+ )
61
+ return
62
+
63
+ title = _extract_title(changelog_path) or "What's New"
64
+
65
+ node = NavNode(
66
+ title=title,
67
+ docs_path=str(changelog_path.relative_to(config.docs_dir)),
68
+ source_path=changelog_path,
69
+ level=0,
70
+ )
71
+
72
+ if not quiet:
73
+ print(f" compiling '{title}' (changelog)")
74
+
75
+ xhtml, attachments, labels, confluence_status, version_message = compile_page(
76
+ node, config, quiet=quiet
77
+ )
78
+
79
+ xhtml_hash = _xhtml_hash(xhtml)
80
+ existing = client.find_page(space_id, title)
81
+
82
+ if existing is not None and client.get_content_hash(str(existing["id"])) == xhtml_hash:
83
+ if not quiet:
84
+ print(f" unchanged '{title}' (changelog)")
85
+ return
86
+
87
+ parent_id = conf_config.parent_page_id
88
+
89
+ if existing is None:
90
+ page = client.create_page(space_id, title, xhtml, parent_id=parent_id)
91
+ page_id = str(page["id"])
92
+ # Do NOT stamp as managed: _prune_orphans skips unmanaged pages, so
93
+ # this ensures --prune never deletes the changelog page.
94
+ if not quiet:
95
+ print(f" created '{title}' (changelog)")
96
+ else:
97
+ page_id = str(existing["id"])
98
+ version: int = existing["version"]["number"]
99
+ client.update_page(
100
+ page_id, title, xhtml, version + 1,
101
+ parent_id=parent_id,
102
+ version_message=version_message,
103
+ )
104
+ if not quiet:
105
+ print(f" updated '{title}' (changelog)")
106
+
107
+ if attachments:
108
+ _upload_assets(page_id, attachments, config.docs_dir, client, quiet=quiet)
109
+
110
+ try:
111
+ client.set_content_hash(page_id, xhtml_hash)
112
+ except Exception:
113
+ pass
114
+
115
+ if labels:
116
+ try:
117
+ client.set_page_labels(page_id, labels)
118
+ except Exception:
119
+ pass
120
+
121
+ if conf_config.full_width:
122
+ try:
123
+ client.set_page_full_width(page_id)
124
+ except Exception:
125
+ pass
126
+
127
+ if confluence_status:
128
+ try:
129
+ client.set_page_status(page_id, confluence_status, space_key=space_key)
130
+ except Exception:
131
+ pass