mkdocs2confluence 0.7.37__tar.gz → 0.8.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 (84) hide show
  1. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/PKG-INFO +43 -1
  2. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/README.md +42 -0
  3. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/pyproject.toml +1 -1
  4. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs2confluence.egg-info/PKG-INFO +43 -1
  5. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs2confluence.egg-info/SOURCES.txt +12 -0
  6. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/cli.py +123 -0
  7. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/loader/config.py +7 -0
  8. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/publisher/client.py +82 -0
  9. mkdocs2confluence-0.8.0/src/mkdocs_to_confluence/sync/__init__.py +1 -0
  10. mkdocs2confluence-0.8.0/src/mkdocs_to_confluence/sync/anchoring.py +25 -0
  11. mkdocs2confluence-0.8.0/src/mkdocs_to_confluence/sync/command.py +236 -0
  12. mkdocs2confluence-0.8.0/src/mkdocs_to_confluence/sync/comments.py +89 -0
  13. mkdocs2confluence-0.8.0/src/mkdocs_to_confluence/sync/github.py +143 -0
  14. mkdocs2confluence-0.8.0/src/mkdocs_to_confluence/sync/platform.py +50 -0
  15. mkdocs2confluence-0.8.0/src/mkdocs_to_confluence/sync/state.py +45 -0
  16. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_publish_pipeline.py +77 -0
  17. mkdocs2confluence-0.8.0/tests/test_sync_anchoring.py +56 -0
  18. mkdocs2confluence-0.8.0/tests/test_sync_command.py +283 -0
  19. mkdocs2confluence-0.8.0/tests/test_sync_comments.py +137 -0
  20. mkdocs2confluence-0.8.0/tests/test_sync_github.py +191 -0
  21. mkdocs2confluence-0.8.0/tests/test_sync_state.py +75 -0
  22. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/LICENSE +0 -0
  23. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/setup.cfg +0 -0
  24. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  25. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  26. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  27. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  28. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/__init__.py +0 -0
  29. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  30. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  31. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  32. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/ir/document.py +0 -0
  33. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  34. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  35. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  36. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  37. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  38. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/loader/page.py +0 -0
  39. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  40. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  41. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  42. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  43. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  44. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  45. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  46. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  47. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  48. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  49. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  50. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  51. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  52. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preview/render.py +0 -0
  53. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/preview/server.py +0 -0
  54. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  55. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/publisher/pipeline.py +0 -0
  56. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  57. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  58. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  59. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  60. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  61. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  62. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  63. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_abbrevs.py +0 -0
  64. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_cli.py +0 -0
  65. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_editlink.py +0 -0
  66. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_emitter.py +0 -0
  67. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_extra_css.py +0 -0
  68. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_frontmatter.py +0 -0
  69. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_icons.py +0 -0
  70. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_images.py +0 -0
  71. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_internallinks.py +0 -0
  72. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_ir.py +0 -0
  73. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_linkdefs.py +0 -0
  74. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_loader.py +0 -0
  75. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_mermaid.py +0 -0
  76. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_page_loader.py +0 -0
  77. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_parser.py +0 -0
  78. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_pdf.py +0 -0
  79. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_preprocess.py +0 -0
  80. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_preview.py +0 -0
  81. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_publish_client.py +0 -0
  82. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_publish_config.py +0 -0
  83. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_server.py +0 -0
  84. {mkdocs2confluence-0.7.37 → mkdocs2confluence-0.8.0}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.7.37
3
+ Version: 0.8.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
@@ -270,6 +270,48 @@ The PDF includes a **cover page**, **table of contents** with page numbers, and
270
270
 
271
271
  ---
272
272
 
273
+ ### `mk2conf sync-comments`
274
+
275
+ Bridge Confluence page/inline comments to GitHub pull request review threads. Non-technical reviewers comment in Confluence; developers address feedback on a GitHub feature branch; Confluence comments are auto-resolved when the PR is merged.
276
+
277
+ ```
278
+ mk2conf sync-comments [--config PATH] [--check-merges] [--force] [--dry-run] [--quiet]
279
+ ```
280
+
281
+ | Flag | Default | Description |
282
+ |---|---|---|
283
+ | `--config PATH` | `./mkdocs.yml` | Path to `mkdocs.yml` |
284
+ | `--check-merges` | off | Check tracked PRs for merges and auto-resolve Confluence comments |
285
+ | `--force` | off | Re-sync pages that already have an open review PR |
286
+ | `--dry-run` | off | Print what would be synced without making any API calls |
287
+ | `--quiet` | off | Suppress progress output |
288
+
289
+ **Required config** (under `extra.confluence` in `mkdocs.yml`):
290
+
291
+ ```yaml
292
+ extra:
293
+ confluence:
294
+ github_repo: owner/repo # required for sync-comments
295
+ github_token: ${GITHUB_TOKEN} # falls back to GITHUB_TOKEN env var
296
+ github_base_branch: main # default: main
297
+ ```
298
+
299
+ **Workflow:**
300
+
301
+ 1. Run `mk2conf publish` first — generates `.mk2conf-pages.json` mapping source files to Confluence page IDs.
302
+ 2. Run `mk2conf sync-comments` — for each page with open Confluence comments, creates a `mk2conf/review/{slug}` branch and PR, then posts each comment as a GitHub review thread. Inline comments with a text selection are anchored to the matching source line; page-level comments fall back to file-level review threads. Every thread body includes a **View in Confluence** deep-link that opens Confluence focused on the exact comment.
303
+ 3. Developer addresses feedback on the branch, pushes changes, and merges the PR.
304
+ 4. Run `mk2conf sync-comments --check-merges` — detects merged PRs, adds a resolution reply to each Confluence comment with the commit info, and marks the comments as resolved.
305
+
306
+ **State files** (add to `.gitignore`):
307
+
308
+ | File | Purpose |
309
+ |---|---|
310
+ | `.mk2conf-pages.json` | Source path → Confluence page ID map, written after each `publish` |
311
+ | `.mk2conf-sync-state.json` | Tracks open/merged review PRs and their associated comment IDs |
312
+
313
+ ---
314
+
273
315
  ## Supported Markdown features
274
316
 
275
317
  ### Block elements
@@ -230,6 +230,48 @@ The PDF includes a **cover page**, **table of contents** with page numbers, and
230
230
 
231
231
  ---
232
232
 
233
+ ### `mk2conf sync-comments`
234
+
235
+ Bridge Confluence page/inline comments to GitHub pull request review threads. Non-technical reviewers comment in Confluence; developers address feedback on a GitHub feature branch; Confluence comments are auto-resolved when the PR is merged.
236
+
237
+ ```
238
+ mk2conf sync-comments [--config PATH] [--check-merges] [--force] [--dry-run] [--quiet]
239
+ ```
240
+
241
+ | Flag | Default | Description |
242
+ |---|---|---|
243
+ | `--config PATH` | `./mkdocs.yml` | Path to `mkdocs.yml` |
244
+ | `--check-merges` | off | Check tracked PRs for merges and auto-resolve Confluence comments |
245
+ | `--force` | off | Re-sync pages that already have an open review PR |
246
+ | `--dry-run` | off | Print what would be synced without making any API calls |
247
+ | `--quiet` | off | Suppress progress output |
248
+
249
+ **Required config** (under `extra.confluence` in `mkdocs.yml`):
250
+
251
+ ```yaml
252
+ extra:
253
+ confluence:
254
+ github_repo: owner/repo # required for sync-comments
255
+ github_token: ${GITHUB_TOKEN} # falls back to GITHUB_TOKEN env var
256
+ github_base_branch: main # default: main
257
+ ```
258
+
259
+ **Workflow:**
260
+
261
+ 1. Run `mk2conf publish` first — generates `.mk2conf-pages.json` mapping source files to Confluence page IDs.
262
+ 2. Run `mk2conf sync-comments` — for each page with open Confluence comments, creates a `mk2conf/review/{slug}` branch and PR, then posts each comment as a GitHub review thread. Inline comments with a text selection are anchored to the matching source line; page-level comments fall back to file-level review threads. Every thread body includes a **View in Confluence** deep-link that opens Confluence focused on the exact comment.
263
+ 3. Developer addresses feedback on the branch, pushes changes, and merges the PR.
264
+ 4. Run `mk2conf sync-comments --check-merges` — detects merged PRs, adds a resolution reply to each Confluence comment with the commit info, and marks the comments as resolved.
265
+
266
+ **State files** (add to `.gitignore`):
267
+
268
+ | File | Purpose |
269
+ |---|---|
270
+ | `.mk2conf-pages.json` | Source path → Confluence page ID map, written after each `publish` |
271
+ | `.mk2conf-sync-state.json` | Tracks open/merged review PRs and their associated comment IDs |
272
+
273
+ ---
274
+
233
275
  ## Supported Markdown features
234
276
 
235
277
  ### Block elements
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.7.37"
3
+ version = "0.8.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" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.7.37
3
+ Version: 0.8.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
@@ -270,6 +270,48 @@ The PDF includes a **cover page**, **table of contents** with page numbers, and
270
270
 
271
271
  ---
272
272
 
273
+ ### `mk2conf sync-comments`
274
+
275
+ Bridge Confluence page/inline comments to GitHub pull request review threads. Non-technical reviewers comment in Confluence; developers address feedback on a GitHub feature branch; Confluence comments are auto-resolved when the PR is merged.
276
+
277
+ ```
278
+ mk2conf sync-comments [--config PATH] [--check-merges] [--force] [--dry-run] [--quiet]
279
+ ```
280
+
281
+ | Flag | Default | Description |
282
+ |---|---|---|
283
+ | `--config PATH` | `./mkdocs.yml` | Path to `mkdocs.yml` |
284
+ | `--check-merges` | off | Check tracked PRs for merges and auto-resolve Confluence comments |
285
+ | `--force` | off | Re-sync pages that already have an open review PR |
286
+ | `--dry-run` | off | Print what would be synced without making any API calls |
287
+ | `--quiet` | off | Suppress progress output |
288
+
289
+ **Required config** (under `extra.confluence` in `mkdocs.yml`):
290
+
291
+ ```yaml
292
+ extra:
293
+ confluence:
294
+ github_repo: owner/repo # required for sync-comments
295
+ github_token: ${GITHUB_TOKEN} # falls back to GITHUB_TOKEN env var
296
+ github_base_branch: main # default: main
297
+ ```
298
+
299
+ **Workflow:**
300
+
301
+ 1. Run `mk2conf publish` first — generates `.mk2conf-pages.json` mapping source files to Confluence page IDs.
302
+ 2. Run `mk2conf sync-comments` — for each page with open Confluence comments, creates a `mk2conf/review/{slug}` branch and PR, then posts each comment as a GitHub review thread. Inline comments with a text selection are anchored to the matching source line; page-level comments fall back to file-level review threads. Every thread body includes a **View in Confluence** deep-link that opens Confluence focused on the exact comment.
303
+ 3. Developer addresses feedback on the branch, pushes changes, and merges the PR.
304
+ 4. Run `mk2conf sync-comments --check-merges` — detects merged PRs, adds a resolution reply to each Confluence comment with the commit info, and marks the comments as resolved.
305
+
306
+ **State files** (add to `.gitignore`):
307
+
308
+ | File | Purpose |
309
+ |---|---|
310
+ | `.mk2conf-pages.json` | Source path → Confluence page ID map, written after each `publish` |
311
+ | `.mk2conf-sync-state.json` | Tracks open/merged review PRs and their associated comment IDs |
312
+
313
+ ---
314
+
273
315
  ## Supported Markdown features
274
316
 
275
317
  ### Block elements
@@ -38,6 +38,13 @@ src/mkdocs_to_confluence/preview/server.py
38
38
  src/mkdocs_to_confluence/publisher/__init__.py
39
39
  src/mkdocs_to_confluence/publisher/client.py
40
40
  src/mkdocs_to_confluence/publisher/pipeline.py
41
+ src/mkdocs_to_confluence/sync/__init__.py
42
+ src/mkdocs_to_confluence/sync/anchoring.py
43
+ src/mkdocs_to_confluence/sync/command.py
44
+ src/mkdocs_to_confluence/sync/comments.py
45
+ src/mkdocs_to_confluence/sync/github.py
46
+ src/mkdocs_to_confluence/sync/platform.py
47
+ src/mkdocs_to_confluence/sync/state.py
41
48
  src/mkdocs_to_confluence/transforms/__init__.py
42
49
  src/mkdocs_to_confluence/transforms/abbrevs.py
43
50
  src/mkdocs_to_confluence/transforms/assets.py
@@ -67,4 +74,9 @@ tests/test_publish_client.py
67
74
  tests/test_publish_config.py
68
75
  tests/test_publish_pipeline.py
69
76
  tests/test_server.py
77
+ tests/test_sync_anchoring.py
78
+ tests/test_sync_command.py
79
+ tests/test_sync_comments.py
80
+ tests/test_sync_github.py
81
+ tests/test_sync_state.py
70
82
  tests/test_treeutil.py
@@ -223,6 +223,48 @@ def _build_parser() -> argparse.ArgumentParser:
223
223
  help="Suppress per-item progress output.",
224
224
  )
225
225
 
226
+ # --- sync-comments ---
227
+ sc = sub.add_parser(
228
+ "sync-comments",
229
+ help="Sync open Confluence comments to GitHub review PRs.",
230
+ formatter_class=argparse.RawDescriptionHelpFormatter,
231
+ epilog=(
232
+ "Requires github_repo and github_token (or GITHUB_TOKEN env var) in mkdocs.yml.\n"
233
+ "\n"
234
+ "Examples:\n"
235
+ " mk2conf sync-comments # sync new comments → PRs\n"
236
+ " mk2conf sync-comments --check-merges # resolve merged PRs in Confluence\n"
237
+ " mk2conf sync-comments --dry-run # preview without making changes\n"
238
+ ),
239
+ )
240
+ sc.add_argument(
241
+ "--config",
242
+ metavar="PATH",
243
+ default="mkdocs.yml",
244
+ help="Path to mkdocs.yml (default: ./mkdocs.yml).",
245
+ )
246
+ sc.add_argument(
247
+ "--check-merges",
248
+ action="store_true",
249
+ help="Check tracked PRs for merges and resolve their Confluence comments.",
250
+ )
251
+ sc.add_argument(
252
+ "--force",
253
+ action="store_true",
254
+ help="Re-sync pages that already have an open review PR.",
255
+ )
256
+ sc.add_argument(
257
+ "--dry-run",
258
+ action="store_true",
259
+ help="Print what would be synced without creating branches, PRs, or review threads.",
260
+ )
261
+ sc.add_argument(
262
+ "--quiet",
263
+ "-q",
264
+ action="store_true",
265
+ help="Suppress progress output.",
266
+ )
267
+
226
268
  return parser
227
269
 
228
270
 
@@ -244,6 +286,8 @@ def main(argv: list[str] | None = None) -> None:
244
286
  _cmd_publish(args)
245
287
  elif args.command == "pdf":
246
288
  _cmd_pdf(args)
289
+ elif args.command == "sync-comments":
290
+ _cmd_sync_comments(args)
247
291
  except (ValueError, FileNotFoundError) as exc:
248
292
  print(f"error: {exc}", file=sys.stderr)
249
293
  sys.exit(1)
@@ -503,6 +547,27 @@ def _cmd_publish(args: argparse.Namespace) -> None:
503
547
 
504
548
  print(str(report))
505
549
 
550
+ # Write page map so sync-comments can match source files to Confluence pages.
551
+ if not (getattr(args, "page", None) or getattr(args, "section", None)):
552
+ import json as _json_pm
553
+ try:
554
+ repo_root = config_path.parent
555
+ try:
556
+ docs_rel = config.docs_dir.relative_to(repo_root)
557
+ except ValueError:
558
+ docs_rel = Path("docs")
559
+ page_map = {
560
+ str(docs_rel / action.node.docs_path): action.page_id
561
+ for action in plan
562
+ if action.node.docs_path and action.page_id and not action.is_folder
563
+ }
564
+ pm_path = repo_root / ".mk2conf-pages.json"
565
+ pm_path.write_text(_json_pm.dumps(page_map, indent=2), encoding="utf-8")
566
+ if not getattr(args, "quiet", False):
567
+ print(f"Page map: {len(page_map)} page(s) → {pm_path.name}")
568
+ except Exception as exc:
569
+ print(f" [warn] could not write page map: {exc}", file=sys.stderr)
570
+
506
571
  if getattr(args, "report", None):
507
572
  import json as _json
508
573
 
@@ -612,3 +677,61 @@ def _cmd_pdf(args: argparse.Namespace) -> None:
612
677
  sys.exit(1)
613
678
 
614
679
  print(f"PDF written to {out_path}")
680
+
681
+
682
+ def _cmd_sync_comments(args: argparse.Namespace) -> None:
683
+ from mkdocs_to_confluence.publisher.client import ConfluenceClient, ConfluenceError
684
+ from mkdocs_to_confluence.sync.command import check_and_resolve_merges, run_sync_comments
685
+ from mkdocs_to_confluence.sync.github import GitHubReviewClient
686
+
687
+ config_path = Path(args.config).resolve()
688
+ config = load_config(config_path)
689
+
690
+ conf = config.confluence
691
+ if conf is None:
692
+ print("error: no 'confluence:' section in mkdocs.yml", file=sys.stderr)
693
+ sys.exit(1)
694
+
695
+ if not conf.token:
696
+ print("error: Confluence API token not set. Set CONFLUENCE_API_TOKEN env var.", file=sys.stderr)
697
+ sys.exit(1)
698
+
699
+ if not conf.github_repo:
700
+ print(
701
+ "error: 'confluence.github_repo' is required for sync-comments (e.g. 'owner/repo').",
702
+ file=sys.stderr,
703
+ )
704
+ sys.exit(1)
705
+
706
+ if not conf.github_token:
707
+ print(
708
+ "error: GitHub token not set. Set GITHUB_TOKEN env var or 'confluence.github_token' in mkdocs.yml.",
709
+ file=sys.stderr,
710
+ )
711
+ sys.exit(1)
712
+
713
+ review_client = GitHubReviewClient(conf.github_repo, conf.github_token)
714
+ config_dir = config_path.parent
715
+
716
+ try:
717
+ with ConfluenceClient(conf) as confluence_client:
718
+ if args.check_merges:
719
+ check_and_resolve_merges(
720
+ config_dir=config_dir,
721
+ confluence_client=confluence_client,
722
+ review_client=review_client,
723
+ quiet=args.quiet,
724
+ )
725
+ else:
726
+ run_sync_comments(
727
+ config=config,
728
+ config_dir=config_dir,
729
+ confluence_client=confluence_client,
730
+ review_client=review_client,
731
+ force=args.force,
732
+ dry_run=args.dry_run,
733
+ quiet=args.quiet,
734
+ )
735
+ except ConfluenceError as exc:
736
+ print(f"error: {exc}", file=sys.stderr)
737
+ sys.exit(1)
@@ -30,6 +30,9 @@ class ConfluenceConfig:
30
30
  full_width: bool = True # set full-width layout on every published page
31
31
  nav_file: str = ".pages" # awesome-pages filename (default: .pages, can be .nav etc.)
32
32
  mermaid_render: str = "kroki" # "kroki", "kroki:<url>", or "none"
33
+ github_repo: str | None = None # "owner/repo" — required for sync-comments
34
+ github_token: str | None = None # GitHub PAT (falls back to GITHUB_TOKEN env var)
35
+ github_base_branch: str = "main" # base branch for review PRs
33
36
 
34
37
 
35
38
  @dataclass(frozen=True)
@@ -241,6 +244,10 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
241
244
  full_width=bool(raw_conf.get("full_width", True)),
242
245
  nav_file=str(raw_conf.get("nav_file", ".pages")),
243
246
  mermaid_render=str(raw_conf.get("mermaid_render", "kroki")),
247
+ github_repo=str(raw_conf["github_repo"]) if raw_conf.get("github_repo") else None,
248
+ github_token=(str(raw_conf["github_token"]) if raw_conf.get("github_token")
249
+ else os.environ.get("GITHUB_TOKEN") or None),
250
+ github_base_branch=str(raw_conf.get("github_base_branch", "main")),
244
251
  )
245
252
 
246
253
  # --- extra_css (optional) ---
@@ -538,3 +538,85 @@ class ConfluenceClient:
538
538
  """Permanently delete *page_id* from Confluence."""
539
539
  resp = self._http.delete(self._v2(f"/pages/{page_id}"))
540
540
  self._raise_for_status(resp, f"delete_page({page_id!r})")
541
+
542
+ # ── Comments ───────────────────────────────────────────────────────────────
543
+
544
+ def get_page_inline_comments(self, page_id: str) -> list[dict[str, Any]]:
545
+ """Return all open inline comments for *page_id* (paginated)."""
546
+ results: list[dict[str, Any]] = []
547
+ url = self._v2(f"/pages/{page_id}/inline-comments")
548
+ params: dict[str, Any] = {"resolution-status": "open", "body-format": "storage", "limit": 250}
549
+ while True:
550
+ resp = self._http.get(url, params=params)
551
+ self._raise_for_status(resp, f"get_page_inline_comments({page_id!r})")
552
+ data = resp.json()
553
+ results.extend(data.get("results", []))
554
+ next_url = data.get("_links", {}).get("next")
555
+ if not next_url:
556
+ break
557
+ url = self._v2(f"/pages/{page_id}/inline-comments")
558
+ params = {"resolution-status": "open", "body-format": "storage",
559
+ "limit": 250, "cursor": _extract_cursor(next_url)}
560
+ return results
561
+
562
+ def get_page_footer_comments(self, page_id: str) -> list[dict[str, Any]]:
563
+ """Return all open footer comments for *page_id* (paginated)."""
564
+ results: list[dict[str, Any]] = []
565
+ url = self._v2(f"/pages/{page_id}/footer-comments")
566
+ params: dict[str, Any] = {"resolution-status": "open", "body-format": "storage", "limit": 250}
567
+ while True:
568
+ resp = self._http.get(url, params=params)
569
+ self._raise_for_status(resp, f"get_page_footer_comments({page_id!r})")
570
+ data = resp.json()
571
+ results.extend(data.get("results", []))
572
+ next_url = data.get("_links", {}).get("next")
573
+ if not next_url:
574
+ break
575
+ url = self._v2(f"/pages/{page_id}/footer-comments")
576
+ params = {"resolution-status": "open", "body-format": "storage",
577
+ "limit": 250, "cursor": _extract_cursor(next_url)}
578
+ return results
579
+
580
+ def add_comment_reply(self, comment_id: str, reply_text: str) -> None:
581
+ """Post a reply to *comment_id* using the v1 content API."""
582
+ url = self._v1(f"/content/{comment_id}/child/comment")
583
+ resp = self._http.post(url, json={
584
+ "type": "comment",
585
+ "body": {
586
+ "storage": {
587
+ "value": f"<p>{reply_text}</p>",
588
+ "representation": "storage",
589
+ }
590
+ },
591
+ })
592
+ self._raise_for_status(resp, f"add_comment_reply({comment_id!r})")
593
+
594
+ def resolve_inline_comment(self, comment_id: str) -> None:
595
+ """Resolve an inline comment by setting *resolved=true*."""
596
+ url = self._v2(f"/inline-comments/{comment_id}")
597
+ get_resp = self._http.get(url, params={"body-format": "storage"})
598
+ self._raise_for_status(get_resp, f"get_inline_comment({comment_id!r})")
599
+ data = get_resp.json()
600
+ version = data.get("version", {}).get("number", 1)
601
+ body_value = data.get("body", {}).get("storage", {}).get("value", "")
602
+ resp = self._http.put(url, json={
603
+ "version": {"number": version + 1},
604
+ "resolved": True,
605
+ "body": {"representation": "storage", "value": body_value},
606
+ })
607
+ self._raise_for_status(resp, f"resolve_inline_comment({comment_id!r})")
608
+
609
+ def resolve_footer_comment(self, comment_id: str) -> None:
610
+ """Resolve a footer comment by setting *resolved=true*."""
611
+ url = self._v2(f"/footer-comments/{comment_id}")
612
+ get_resp = self._http.get(url, params={"body-format": "storage"})
613
+ self._raise_for_status(get_resp, f"get_footer_comment({comment_id!r})")
614
+ data = get_resp.json()
615
+ version = data.get("version", {}).get("number", 1)
616
+ body_value = data.get("body", {}).get("storage", {}).get("value", "")
617
+ resp = self._http.put(url, json={
618
+ "version": {"number": version + 1},
619
+ "resolved": True,
620
+ "body": {"representation": "storage", "value": body_value},
621
+ })
622
+ self._raise_for_status(resp, f"resolve_footer_comment({comment_id!r})")
@@ -0,0 +1 @@
1
+ """Confluence ↔ GitHub comment synchronisation."""
@@ -0,0 +1,25 @@
1
+ """Map Confluence inlineOriginalSelection text to a source file line number."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def find_anchor_line(source_path: Path, selection_text: str) -> int | None:
9
+ """Return the 1-based line number of the first line containing *selection_text*.
10
+
11
+ Returns ``None`` when:
12
+ - *selection_text* is empty
13
+ - the file cannot be read
14
+ - no line contains the text
15
+ """
16
+ if not selection_text:
17
+ return None
18
+ try:
19
+ lines = source_path.read_text(encoding="utf-8").splitlines()
20
+ except OSError:
21
+ return None
22
+ for i, line in enumerate(lines, start=1):
23
+ if selection_text in line:
24
+ return i
25
+ return None