mkdocs2confluence 0.6.2__tar.gz → 0.6.4__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 (66) hide show
  1. {mkdocs2confluence-0.6.2/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.6.4}/PKG-INFO +7 -5
  2. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/README.md +6 -4
  3. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/pyproject.toml +1 -1
  4. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4/src/mkdocs2confluence.egg-info}/PKG-INFO +7 -5
  5. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/SOURCES.txt +1 -0
  6. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/cli.py +44 -11
  7. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/nav.py +15 -8
  8. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/publisher/pipeline.py +71 -38
  9. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/mermaid.py +46 -18
  10. mkdocs2confluence-0.6.4/tests/test_cli.py +196 -0
  11. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_loader.py +22 -0
  12. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_mermaid.py +100 -4
  13. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_preview.py +0 -2
  14. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_publish_pipeline.py +64 -4
  15. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/LICENSE +0 -0
  16. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/setup.cfg +0 -0
  17. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  18. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  19. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  20. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  21. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/__init__.py +0 -0
  22. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  23. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  24. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  25. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/ir/document.py +0 -0
  26. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  27. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  28. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  29. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/config.py +0 -0
  30. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  31. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/page.py +0 -0
  32. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  33. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  34. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  35. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  36. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  37. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  38. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  39. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  40. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  41. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  42. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preview/render.py +0 -0
  43. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  44. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  45. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  46. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  47. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  48. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  49. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  50. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  51. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_abbrevs.py +0 -0
  52. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_editlink.py +0 -0
  53. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_emitter.py +0 -0
  54. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_extra_css.py +0 -0
  55. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_frontmatter.py +0 -0
  56. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_icons.py +0 -0
  57. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_images.py +0 -0
  58. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_internallinks.py +0 -0
  59. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_ir.py +0 -0
  60. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_linkdefs.py +0 -0
  61. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_page_loader.py +0 -0
  62. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_parser.py +0 -0
  63. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_preprocess.py +0 -0
  64. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_publish_client.py +0 -0
  65. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_publish_config.py +0 -0
  66. {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.6.2
3
+ Version: 0.6.4
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
@@ -88,6 +88,8 @@ cd mkdocs2confluence
88
88
  pip install -e ".[dev]"
89
89
  ```
90
90
 
91
+ > **Package name vs. command name** — This follows the same convention used by many popular CLI tools (e.g. `pip install httpie` → `http` command). The PyPI package is `mkdocs2confluence` (descriptive, easy to find), and the CLI command is `mk2conf` (short, fast to type). Install once, run everywhere as `mk2conf`.
92
+
91
93
  ---
92
94
 
93
95
  ## Quick start
@@ -112,7 +114,7 @@ CONFLUENCE_API_TOKEN=your_token mk2conf publish --config mkdocs.yml
112
114
 
113
115
  ### `mk2conf preview`
114
116
 
115
- Compile a single page and inspect the output — no network connection required.
117
+ Compile a single page and inspect the output — no Confluence API calls required. Mermaid diagrams are rendered via Kroki unless `mermaid_render: none` is set.
116
118
 
117
119
  ```
118
120
  mk2conf preview [--config PATH] --page PATH [--out FILE] [--html]
@@ -142,7 +144,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
142
144
  | `--config PATH` | `./mkdocs.yml` | Path to your `mkdocs.yml` |
143
145
  | `--page PATH` | *(all nav pages)* | Publish a single page only |
144
146
  | `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
145
- | `--dry-run` | off | Print the publish plan without making any API calls |
147
+ | `--dry-run` | off | Print the publish plan without making any Confluence API calls (Mermaid diagrams are still rendered via Kroki unless `mermaid_render: none` is set) |
146
148
  | `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
147
149
  | `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
148
150
 
@@ -172,7 +174,7 @@ The API token is read from (in priority order):
172
174
  - Pages with `ready: false` in their YAML front matter are **skipped**, even if listed in the nav.
173
175
  - Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
174
176
  - All locally linked assets are uploaded as Confluence page attachments automatically.
175
- - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification.
177
+ - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification. As a side effect, Confluence's built-in version history becomes a meaningful audit trail: every version represents a real content change, each stamped *"Updated by mk2conf"* so automated publishes are clearly distinguishable from manual edits. You can diff any two versions directly inside Confluence, with inline highlighting showing exactly which paragraphs, headings, or code blocks changed.
176
178
  - **Orphan pruning** — every page created by mk2conf is stamped with a hidden `mk2conf-managed` property. Pass `--prune` to automatically delete managed pages that have been removed from `nav:`. Manually-created Confluence pages are never deleted.
177
179
 
178
180
  #### Orphan pruning
@@ -346,7 +348,7 @@ See [Setup.md](Setup.md) for environment setup.
346
348
 
347
349
  ```bash
348
350
  pytest # run tests
349
- ruff check src # lint
351
+ ruff check src tests # lint
350
352
  mypy src # type-check
351
353
  bandit -r src -ll # security scan
352
354
  ```
@@ -50,6 +50,8 @@ cd mkdocs2confluence
50
50
  pip install -e ".[dev]"
51
51
  ```
52
52
 
53
+ > **Package name vs. command name** — This follows the same convention used by many popular CLI tools (e.g. `pip install httpie` → `http` command). The PyPI package is `mkdocs2confluence` (descriptive, easy to find), and the CLI command is `mk2conf` (short, fast to type). Install once, run everywhere as `mk2conf`.
54
+
53
55
  ---
54
56
 
55
57
  ## Quick start
@@ -74,7 +76,7 @@ CONFLUENCE_API_TOKEN=your_token mk2conf publish --config mkdocs.yml
74
76
 
75
77
  ### `mk2conf preview`
76
78
 
77
- Compile a single page and inspect the output — no network connection required.
79
+ Compile a single page and inspect the output — no Confluence API calls required. Mermaid diagrams are rendered via Kroki unless `mermaid_render: none` is set.
78
80
 
79
81
  ```
80
82
  mk2conf preview [--config PATH] --page PATH [--out FILE] [--html]
@@ -104,7 +106,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
104
106
  | `--config PATH` | `./mkdocs.yml` | Path to your `mkdocs.yml` |
105
107
  | `--page PATH` | *(all nav pages)* | Publish a single page only |
106
108
  | `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
107
- | `--dry-run` | off | Print the publish plan without making any API calls |
109
+ | `--dry-run` | off | Print the publish plan without making any Confluence API calls (Mermaid diagrams are still rendered via Kroki unless `mermaid_render: none` is set) |
108
110
  | `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
109
111
  | `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
110
112
 
@@ -134,7 +136,7 @@ The API token is read from (in priority order):
134
136
  - Pages with `ready: false` in their YAML front matter are **skipped**, even if listed in the nav.
135
137
  - Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
136
138
  - All locally linked assets are uploaded as Confluence page attachments automatically.
137
- - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification.
139
+ - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification. As a side effect, Confluence's built-in version history becomes a meaningful audit trail: every version represents a real content change, each stamped *"Updated by mk2conf"* so automated publishes are clearly distinguishable from manual edits. You can diff any two versions directly inside Confluence, with inline highlighting showing exactly which paragraphs, headings, or code blocks changed.
138
140
  - **Orphan pruning** — every page created by mk2conf is stamped with a hidden `mk2conf-managed` property. Pass `--prune` to automatically delete managed pages that have been removed from `nav:`. Manually-created Confluence pages are never deleted.
139
141
 
140
142
  #### Orphan pruning
@@ -308,7 +310,7 @@ See [Setup.md](Setup.md) for environment setup.
308
310
 
309
311
  ```bash
310
312
  pytest # run tests
311
- ruff check src # lint
313
+ ruff check src tests # lint
312
314
  mypy src # type-check
313
315
  bandit -r src -ll # security scan
314
316
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.6.2"
3
+ version = "0.6.4"
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" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.6.2
3
+ Version: 0.6.4
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
@@ -88,6 +88,8 @@ cd mkdocs2confluence
88
88
  pip install -e ".[dev]"
89
89
  ```
90
90
 
91
+ > **Package name vs. command name** — This follows the same convention used by many popular CLI tools (e.g. `pip install httpie` → `http` command). The PyPI package is `mkdocs2confluence` (descriptive, easy to find), and the CLI command is `mk2conf` (short, fast to type). Install once, run everywhere as `mk2conf`.
92
+
91
93
  ---
92
94
 
93
95
  ## Quick start
@@ -112,7 +114,7 @@ CONFLUENCE_API_TOKEN=your_token mk2conf publish --config mkdocs.yml
112
114
 
113
115
  ### `mk2conf preview`
114
116
 
115
- Compile a single page and inspect the output — no network connection required.
117
+ Compile a single page and inspect the output — no Confluence API calls required. Mermaid diagrams are rendered via Kroki unless `mermaid_render: none` is set.
116
118
 
117
119
  ```
118
120
  mk2conf preview [--config PATH] --page PATH [--out FILE] [--html]
@@ -142,7 +144,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
142
144
  | `--config PATH` | `./mkdocs.yml` | Path to your `mkdocs.yml` |
143
145
  | `--page PATH` | *(all nav pages)* | Publish a single page only |
144
146
  | `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
145
- | `--dry-run` | off | Print the publish plan without making any API calls |
147
+ | `--dry-run` | off | Print the publish plan without making any Confluence API calls (Mermaid diagrams are still rendered via Kroki unless `mermaid_render: none` is set) |
146
148
  | `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
147
149
  | `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
148
150
 
@@ -172,7 +174,7 @@ The API token is read from (in priority order):
172
174
  - Pages with `ready: false` in their YAML front matter are **skipped**, even if listed in the nav.
173
175
  - Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
174
176
  - All locally linked assets are uploaded as Confluence page attachments automatically.
175
- - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification.
177
+ - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification. As a side effect, Confluence's built-in version history becomes a meaningful audit trail: every version represents a real content change, each stamped *"Updated by mk2conf"* so automated publishes are clearly distinguishable from manual edits. You can diff any two versions directly inside Confluence, with inline highlighting showing exactly which paragraphs, headings, or code blocks changed.
176
178
  - **Orphan pruning** — every page created by mk2conf is stamped with a hidden `mk2conf-managed` property. Pass `--prune` to automatically delete managed pages that have been removed from `nav:`. Manually-created Confluence pages are never deleted.
177
179
 
178
180
  #### Orphan pruning
@@ -346,7 +348,7 @@ See [Setup.md](Setup.md) for environment setup.
346
348
 
347
349
  ```bash
348
350
  pytest # run tests
349
- ruff check src # lint
351
+ ruff check src tests # lint
350
352
  mypy src # type-check
351
353
  bandit -r src -ll # security scan
352
354
  ```
@@ -42,6 +42,7 @@ src/mkdocs_to_confluence/transforms/images.py
42
42
  src/mkdocs_to_confluence/transforms/internallinks.py
43
43
  src/mkdocs_to_confluence/transforms/mermaid.py
44
44
  tests/test_abbrevs.py
45
+ tests/test_cli.py
45
46
  tests/test_editlink.py
46
47
  tests/test_emitter.py
47
48
  tests/test_extra_css.py
@@ -33,7 +33,17 @@ def _build_parser() -> argparse.ArgumentParser:
33
33
  # --- preview ---
34
34
  preview = sub.add_parser(
35
35
  "preview",
36
- help="Compile a single page and print Confluence XHTML to stdout (no network).",
36
+ help="Compile a page (or whole section) and inspect the output no Confluence API calls.",
37
+ formatter_class=argparse.RawDescriptionHelpFormatter,
38
+ epilog=(
39
+ "Examples:\n"
40
+ " mk2conf preview --page index.md\n"
41
+ " mk2conf preview --page guide/setup.md --html --out /tmp/setup.html\n"
42
+ " mk2conf preview --section Guide\n"
43
+ "\n"
44
+ " Either --page or --section is required.\n"
45
+ " Mermaid diagrams are rendered via Kroki unless 'mermaid_render: none' is set.\n"
46
+ ),
37
47
  )
38
48
  preview.add_argument(
39
49
  "--config",
@@ -47,7 +57,7 @@ def _build_parser() -> argparse.ArgumentParser:
47
57
  default=None,
48
58
  help=(
49
59
  "Relative path to the markdown file to compile. "
50
- "Omit when --section is given to preview the whole section."
60
+ "Required unless --section is given."
51
61
  ),
52
62
  )
53
63
  preview.add_argument(
@@ -65,7 +75,17 @@ def _build_parser() -> argparse.ArgumentParser:
65
75
  "--section",
66
76
  metavar="NAME",
67
77
  default=None,
68
- help="Restrict link map to a nav section (slash-separated path, e.g. 'Guide/Setup').",
78
+ help=(
79
+ "Nav section to preview (slash-separated path, e.g. 'Guide' or 'Guide/Setup'). "
80
+ "Without --page, renders all pages in the section as a browseable HTML index."
81
+ ),
82
+ )
83
+
84
+ preview.add_argument(
85
+ "--quiet",
86
+ "-q",
87
+ action="store_true",
88
+ help="Suppress per-item progress output; only the final summary and warnings are shown.",
69
89
  )
70
90
 
71
91
  # --- publish ---
@@ -125,6 +145,13 @@ def _build_parser() -> argparse.ArgumentParser:
125
145
  ),
126
146
  )
127
147
 
148
+ publish.add_argument(
149
+ "--quiet",
150
+ "-q",
151
+ action="store_true",
152
+ help="Suppress per-item progress output; only the final summary and warnings are shown.",
153
+ )
154
+
128
155
  return parser
129
156
 
130
157
 
@@ -136,12 +163,17 @@ def main(argv: list[str] | None = None) -> None:
136
163
  parser.print_help()
137
164
  sys.exit(0)
138
165
 
139
- print(f"mk2conf {__version__}")
166
+ if sys.stdout.isatty():
167
+ print(f"mk2conf {__version__}")
140
168
 
141
- if args.command == "preview":
142
- _cmd_preview(args)
143
- elif args.command == "publish":
144
- _cmd_publish(args)
169
+ try:
170
+ if args.command == "preview":
171
+ _cmd_preview(args)
172
+ elif args.command == "publish":
173
+ _cmd_publish(args)
174
+ except (ValueError, FileNotFoundError) as exc:
175
+ print(f"error: {exc}", file=sys.stderr)
176
+ sys.exit(1)
145
177
 
146
178
 
147
179
  def _parse_out_path(out_arg: str | None) -> tuple[Path, str]:
@@ -202,7 +234,7 @@ def _cmd_preview(args: argparse.Namespace) -> None:
202
234
  for node in pages:
203
235
  html_name = page_link_map.get(node.title, f"{Path(node.docs_path or node.title).stem}.html")
204
236
  try:
205
- xhtml, _attachments, _labels = compile_page(node, config, link_map)
237
+ xhtml, _attachments, _labels = compile_page(node, config, link_map, quiet=args.quiet)
206
238
  except PageLoadError as exc:
207
239
  print(f" warning: skipping '{node.title}': {exc}", file=sys.stderr)
208
240
  continue
@@ -227,7 +259,7 @@ def _cmd_preview(args: argparse.Namespace) -> None:
227
259
 
228
260
  try:
229
261
  link_map = build_link_map(nodes)
230
- xhtml, _attachments, _labels = compile_page(page_node, config, link_map)
262
+ xhtml, _attachments, _labels = compile_page(page_node, config, link_map, quiet=args.quiet)
231
263
  except PageLoadError as exc:
232
264
  print(f"error: {exc}", file=sys.stderr)
233
265
  sys.exit(1)
@@ -316,7 +348,7 @@ def _cmd_publish(args: argparse.Namespace) -> None:
316
348
  file=sys.stderr,
317
349
  )
318
350
  sys.exit(1)
319
- plan = plan_publish(nav_nodes, client, config, conf_config, space_id=space_id)
351
+ plan = plan_publish(nav_nodes, client, config, conf_config, space_id=space_id, quiet=args.quiet)
320
352
  # --prune is silently disabled for partial publishes (--page / --section)
321
353
  # because published_ids would only cover the subset, not the full nav.
322
354
  partial = bool(getattr(args, "page", None) or getattr(args, "section", None))
@@ -325,6 +357,7 @@ def _cmd_publish(args: argparse.Namespace) -> None:
325
357
  docs_dir=config.docs_dir, full_width=conf_config.full_width,
326
358
  root_page_id=conf_config.parent_page_id,
327
359
  prune=getattr(args, "prune", False) and not partial,
360
+ quiet=args.quiet,
328
361
  )
329
362
  except ConfluenceError as exc:
330
363
  print(f"error: {exc}", file=sys.stderr)
@@ -82,19 +82,26 @@ def _discover(docs_dir: Path, nav_file: str) -> list[NavNode]:
82
82
 
83
83
 
84
84
  def _read_nav_file(directory: Path, nav_file: str) -> list[Any] | None:
85
- """Return the nav list from *directory*/<nav_file>, or None if absent/unreadable."""
85
+ """Return the nav list from *directory*/<nav_file>, or None if absent.
86
+
87
+ Raises ValueError if the file exists but cannot be parsed or has an
88
+ unexpected format — a present-but-broken nav file is a configuration
89
+ error, not a reason to silently fall back to full discovery.
90
+ """
86
91
  path = directory / nav_file
87
92
  if not path.exists():
88
93
  return None
89
94
  try:
90
95
  data = yaml.safe_load(path.read_text(encoding="utf-8"))
91
- if isinstance(data, dict) and isinstance(data.get("nav"), list):
92
- return cast(list[Any], data["nav"])
93
- if isinstance(data, list):
94
- return data
95
- except Exception:
96
- pass
97
- return None
96
+ except Exception as exc:
97
+ raise ValueError(f"Could not parse nav file {path}: {exc}") from exc
98
+ if isinstance(data, dict) and isinstance(data.get("nav"), list):
99
+ return cast(list[Any], data["nav"])
100
+ if isinstance(data, list):
101
+ return data
102
+ raise ValueError(
103
+ f"Nav file {path} has unexpected format (expected a list or a dict with a 'nav' key, got {type(data).__name__})"
104
+ )
98
105
 
99
106
 
100
107
  def _traverse_nav_dir(
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import hashlib
17
17
  import re
18
+ import sys
18
19
  from dataclasses import dataclass, field
19
20
  from datetime import datetime, timezone
20
21
  from pathlib import Path
@@ -134,6 +135,8 @@ def compile_page(
134
135
  node: NavNode,
135
136
  config: MkDocsConfig,
136
137
  link_map: dict[str, str] | None = None,
138
+ *,
139
+ quiet: bool = False,
137
140
  ) -> tuple[str, list[Path], tuple[str, ...]]:
138
141
  """Run the full compile pipeline for one page.
139
142
 
@@ -174,7 +177,7 @@ def compile_page(
174
177
  mermaid_render[len("kroki:"):] if mermaid_render.startswith("kroki:")
175
178
  else DEFAULT_KROKI_URL
176
179
  )
177
- ir_nodes, mermaid_attachments = render_mermaid_diagrams(ir_nodes, kroki_url)
180
+ ir_nodes, mermaid_attachments = render_mermaid_diagrams(ir_nodes, kroki_url, quiet=quiet)
178
181
  attachments = attachments + mermaid_attachments
179
182
  effective_link_map = link_map if link_map is not None else {}
180
183
  if node.docs_path:
@@ -220,6 +223,7 @@ def plan_publish(
220
223
  conf_config: ConfluenceConfig,
221
224
  *,
222
225
  space_id: str,
226
+ quiet: bool = False,
223
227
  ) -> list[PageAction]:
224
228
  """Build a publish plan for the entire nav tree.
225
229
 
@@ -229,8 +233,9 @@ def plan_publish(
229
233
  """
230
234
  actions: list[PageAction] = []
231
235
  link_map = build_link_map(nav_nodes)
232
- print("Planning...")
233
- _plan_nodes(nav_nodes, client, config, space_id, conf_config.parent_page_id, False, actions, link_map)
236
+ if not quiet:
237
+ print("Planning...")
238
+ _plan_nodes(nav_nodes, client, config, space_id, conf_config.parent_page_id, False, actions, link_map, quiet=quiet)
234
239
  return actions
235
240
 
236
241
 
@@ -243,6 +248,8 @@ def _plan_nodes(
243
248
  parent_is_folder: bool,
244
249
  actions: list[PageAction],
245
250
  link_map: dict[str, str] | None = None,
251
+ *,
252
+ quiet: bool = False,
246
253
  ) -> None:
247
254
  for node in nodes:
248
255
  # Strip icon shortcodes from titles — nav titles bypass the body
@@ -261,13 +268,15 @@ def _plan_nodes(
261
268
  except OSError:
262
269
  pass
263
270
  if ready is not False and index_child.source_path is not None:
264
- print(f" compiling '{clean_title}' (section index)")
271
+ if not quiet:
272
+ print(f" compiling '{clean_title}' (section index)")
265
273
  try:
266
- xhtml, attachments, labels = compile_page(index_child, config, link_map)
274
+ xhtml, attachments, labels = compile_page(index_child, config, link_map, quiet=quiet)
267
275
  existing = client.find_page(space_id, clean_title)
268
276
  xhtml_h = _xhtml_hash(xhtml)
269
277
  if existing is not None and client.get_content_hash(str(existing["id"])) == xhtml_h:
270
- print(f" unchanged '{clean_title}' (content unchanged)")
278
+ if not quiet:
279
+ print(f" unchanged '{clean_title}' (content unchanged)")
271
280
  actions.append(PageAction(
272
281
  node=node,
273
282
  title=clean_title,
@@ -278,7 +287,8 @@ def _plan_nodes(
278
287
  ))
279
288
  non_index = [c for c in node.children if c is not index_child]
280
289
  _plan_nodes(
281
- non_index, client, config, space_id, str(existing["id"]), False, actions, link_map
290
+ non_index, client, config, space_id,
291
+ str(existing["id"]), False, actions, link_map, quiet=quiet
282
292
  )
283
293
  continue
284
294
  page_action = PageAction(
@@ -301,16 +311,18 @@ def _plan_nodes(
301
311
  # Recurse remaining children — index.md is already consumed.
302
312
  non_index = [c for c in node.children if c is not index_child]
303
313
  _plan_nodes(
304
- non_index, client, config, space_id, None, False, actions, link_map
314
+ non_index, client, config, space_id, None, False, actions, link_map, quiet=quiet
305
315
  )
306
316
  continue
307
317
  except (PageLoadError, OSError) as exc:
308
318
  print(
309
- f" warning '{clean_title}' index.md error ({exc}),"
310
- " falling back to folder"
319
+ f" [warn] '{clean_title}' index.md load error ({exc}),"
320
+ " falling back to folder",
321
+ file=sys.stderr,
311
322
  )
312
323
 
313
- print(f" compiling '{clean_title}' (folder)")
324
+ if not quiet:
325
+ print(f" compiling '{clean_title}' (folder)")
314
326
  # Folder find-or-create is deferred to execute_publish once the
315
327
  # parent folder ID is known (nested folders don't have a parent ID
316
328
  # yet at plan time).
@@ -327,7 +339,7 @@ def _plan_nodes(
327
339
  actions.append(page_action)
328
340
  # Children will be placed under this folder's ID (resolved at execute)
329
341
  _plan_nodes(
330
- list(node.children), client, config, space_id, None, True, actions, link_map
342
+ list(node.children), client, config, space_id, None, True, actions, link_map, quiet=quiet
331
343
  )
332
344
  else:
333
345
  # Page node — read raw to check ready flag
@@ -340,7 +352,8 @@ def _plan_nodes(
340
352
  pass
341
353
 
342
354
  if ready is False:
343
- print(f" skipping '{clean_title}' (ready: false)")
355
+ if not quiet:
356
+ print(f" skipping '{clean_title}' (ready: false)")
344
357
  actions.append(
345
358
  PageAction(
346
359
  node=node,
@@ -351,11 +364,13 @@ def _plan_nodes(
351
364
  )
352
365
  continue
353
366
 
354
- print(f" compiling '{clean_title}'")
367
+ if not quiet:
368
+ print(f" compiling '{clean_title}'")
355
369
  try:
356
- xhtml, attachments, labels = compile_page(node, config, link_map)
370
+ xhtml, attachments, labels = compile_page(node, config, link_map, quiet=quiet)
357
371
  except (PageLoadError, OSError) as exc:
358
- print(f" skipping '{clean_title}' (error: {exc})")
372
+ if not quiet:
373
+ print(f" skipping '{clean_title}' (error: {exc})")
359
374
  actions.append(
360
375
  PageAction(
361
376
  node=node,
@@ -369,7 +384,8 @@ def _plan_nodes(
369
384
  existing = client.find_page(space_id, clean_title)
370
385
  xhtml_h = _xhtml_hash(xhtml)
371
386
  if existing is not None and client.get_content_hash(str(existing["id"])) == xhtml_h:
372
- print(f" unchanged '{clean_title}' (content unchanged)")
387
+ if not quiet:
388
+ print(f" unchanged '{clean_title}' (content unchanged)")
373
389
  actions.append(
374
390
  PageAction(
375
391
  node=node,
@@ -406,6 +422,8 @@ def _upload_assets(
406
422
  attachments: list[Path],
407
423
  docs_dir: Path,
408
424
  client: ConfluenceClient,
425
+ *,
426
+ quiet: bool = False,
409
427
  ) -> tuple[int, int, list[tuple[str, str]]]:
410
428
  """Upload attachments for one page **sequentially**.
411
429
 
@@ -442,13 +460,15 @@ def _upload_assets(
442
460
  path.stat().st_mtime, tz=timezone.utc
443
461
  )
444
462
  if local_mtime <= confluence_ts:
445
- print(f" skipping {name} (unchanged)")
463
+ if not quiet:
464
+ print(f" skipping {name} (unchanged)")
446
465
  skipped += 1
447
466
  continue
448
467
  except (KeyError, ValueError, OSError):
449
468
  pass # can't compare — fall through to upload
450
469
 
451
- print(f" uploading {name}")
470
+ if not quiet:
471
+ print(f" uploading {name}")
452
472
  try:
453
473
  client.upload_attachment(page_id, path, name, existing)
454
474
  uploaded += 1
@@ -464,6 +484,8 @@ def _execute_folder_action(
464
484
  space_id: str,
465
485
  root_page_id: str | None,
466
486
  report: PublishReport,
487
+ *,
488
+ quiet: bool = False,
467
489
  ) -> None:
468
490
  """Handle folder create/find for a single folder action."""
469
491
  if action.page_id is not None:
@@ -482,8 +504,9 @@ def _execute_folder_action(
482
504
  )
483
505
  except Exception as find_exc:
484
506
  print(
485
- f" [warn] find_folder_under failed "
486
- f"(parent_id={action.parent_id}): {find_exc}"
507
+ f" [warn] find_folder_under failed "
508
+ f"(parent_id={action.parent_id}): {find_exc}",
509
+ file=sys.stderr,
487
510
  )
488
511
  if existing_folder is not None:
489
512
  action.page_id = str(existing_folder["id"])
@@ -497,11 +520,12 @@ def _execute_folder_action(
497
520
  )
498
521
  action.page_id = str(folder["id"])
499
522
  report.created += 1
500
- print(
501
- f" folder id={action.page_id}"
502
- f" parent_id={action.parent_id}"
503
- f" parent_is_folder={action.parent_is_folder}"
504
- )
523
+ if not quiet:
524
+ print(
525
+ f" folder id={action.page_id}"
526
+ f" parent_id={action.parent_id}"
527
+ f" parent_is_folder={action.parent_is_folder}"
528
+ )
505
529
  else:
506
530
  # Parent is a dynamically-created page (e.g. a section-index page).
507
531
  # Confluence folders cannot be nested under pages — use a stub page.
@@ -563,8 +587,9 @@ def _execute_page_action(
563
587
  if not is_stale:
564
588
  raise
565
589
  print(
566
- f" [warn] update failed ({err[:80].strip()}) —"
567
- " stale page_id; falling back to create"
590
+ f" [warn] update failed ({err[:80].strip()}) —"
591
+ " stale page_id; falling back to create",
592
+ file=sys.stderr,
568
593
  )
569
594
  action.page_id = None
570
595
  page = client.create_page(
@@ -602,6 +627,7 @@ def _post_process_action(
602
627
  full_width: bool,
603
628
  docs_dir: Path,
604
629
  report: PublishReport,
630
+ quiet: bool = False,
605
631
  ) -> None:
606
632
  """Run all non-fatal post-create/update work for a single action."""
607
633
  # Store content hash after create/update so the next run can skip unchanged pages.
@@ -628,7 +654,7 @@ def _post_process_action(
628
654
  # Upload assets — skip files whose mtime is not newer than Confluence.
629
655
  if action.page_id and action.attachments:
630
656
  uploaded, asset_skipped, asset_errors = _upload_assets(
631
- action.page_id, action.attachments, docs_dir, client
657
+ action.page_id, action.attachments, docs_dir, client, quiet=quiet
632
658
  )
633
659
  report.assets_uploaded += uploaded
634
660
  report.assets_skipped += asset_skipped
@@ -646,6 +672,7 @@ def execute_publish(
646
672
  full_width: bool = True,
647
673
  root_page_id: str | None = None,
648
674
  prune: bool = False,
675
+ quiet: bool = False,
649
676
  ) -> PublishReport:
650
677
  """Execute the publish plan.
651
678
 
@@ -674,7 +701,8 @@ def execute_publish(
674
701
 
675
702
  active = [a for a in plan if a.action != "skip"]
676
703
  total = len(active)
677
- print(f"\nPublishing {total} page(s)...")
704
+ if not quiet:
705
+ print(f"\nPublishing {total} page(s)...")
678
706
  counter = 0
679
707
 
680
708
  for action in plan:
@@ -683,11 +711,12 @@ def execute_publish(
683
711
  continue
684
712
 
685
713
  counter += 1
686
- print(f" [{counter}/{total}] {action.action:<6} '{action.title}'")
714
+ if not quiet:
715
+ print(f" [{counter}/{total}] {action.action:<6} '{action.title}'")
687
716
 
688
717
  try:
689
718
  if action.is_folder:
690
- _execute_folder_action(action, client, space_id, root_page_id, report)
719
+ _execute_folder_action(action, client, space_id, root_page_id, report, quiet=quiet)
691
720
  else:
692
721
  _execute_page_action(action, client, space_id, report)
693
722
  except Exception as exc:
@@ -700,11 +729,11 @@ def execute_publish(
700
729
  if action.node.is_section and action.page_id:
701
730
  _wire_children(action, action_by_node)
702
731
 
703
- _post_process_action(action, client, full_width=full_width, docs_dir=docs_dir, report=report)
732
+ _post_process_action(action, client, full_width=full_width, docs_dir=docs_dir, report=report, quiet=quiet)
704
733
 
705
734
  if prune and root_page_id:
706
735
  published_ids = {a.page_id for a in plan if a.page_id}
707
- _prune_orphans(client, root_page_id, published_ids, report)
736
+ _prune_orphans(client, root_page_id, published_ids, report, quiet=quiet)
708
737
 
709
738
  return report
710
739
 
@@ -714,6 +743,8 @@ def _prune_orphans(
714
743
  root_page_id: str,
715
744
  published_ids: set[str],
716
745
  report: PublishReport,
746
+ *,
747
+ quiet: bool = False,
717
748
  ) -> None:
718
749
  """Delete managed descendant pages that are no longer in the publish plan.
719
750
 
@@ -723,20 +754,22 @@ def _prune_orphans(
723
754
  try:
724
755
  all_descendants = client.get_descendant_ids(root_page_id)
725
756
  except Exception as exc:
726
- print(f" [warn] prune: could not fetch descendants — {exc}")
757
+ print(f" [warn] prune: could not fetch descendants — {exc}", file=sys.stderr)
727
758
  return
728
759
 
729
760
  orphan_candidates = [pid for pid in all_descendants if pid not in published_ids]
730
761
  if not orphan_candidates:
731
762
  return
732
763
 
733
- print(f"\nPruning: checking {len(orphan_candidates)} orphan candidate(s)...")
764
+ if not quiet:
765
+ print(f"\nPruning: checking {len(orphan_candidates)} orphan candidate(s)...")
734
766
  for page_id in orphan_candidates:
735
767
  try:
736
768
  if not client.is_managed(page_id):
737
769
  continue
738
770
  client.delete_page(page_id)
739
771
  report.pruned += 1
740
- print(f" deleted orphan page {page_id}")
772
+ if not quiet:
773
+ print(f" deleted orphan page {page_id}")
741
774
  except Exception as exc:
742
- print(f" [warn] prune: failed to delete page {page_id} — {exc}")
775
+ print(f" [warn] prune: failed to delete page {page_id} — {exc}", file=sys.stderr)