mkdocs2confluence 0.18.0__tar.gz → 0.19.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.
- {mkdocs2confluence-0.18.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.19.0}/PKG-INFO +3 -3
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/pyproject.toml +20 -5
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0/src/mkdocs2confluence.egg-info}/PKG-INFO +3 -3
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs2confluence.egg-info/requires.txt +2 -2
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/cli.py +1 -1
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/config.py +6 -2
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/parser/markdown.py +117 -23
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/includes.py +1 -1
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/client.py +4 -1
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/skill_installer.py +2 -2
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/_kroki.py +4 -1
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/mermaid.py +1 -1
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_parser.py +208 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_skill_installer.py +2 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/LICENSE +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/README.md +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/setup.cfg +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/compiler/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/compiler/models.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/compiler/page.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/nav.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/pdf/render.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/captions.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preview/server.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/changelog.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/executor.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/http_retry.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/models.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/planner.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/skills/mkdocs-changelog/SKILL.md +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/skills/mkdocs-changelog/scripts/changelog_data.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/command.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/comments.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/github.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/platform.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/state.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/admonition_titles.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/attachment_previews.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/captions.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/plantuml.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_admonition_titles.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_attachment_previews.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_captions.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_changelog_config.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_changelog_publish.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_children_macro.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_cli.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_emitter.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_exclude_properties_config.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_footer.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_images.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_loader.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_mermaid.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_pdf.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_plantuml.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_preprocess.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_preview.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_publish_client.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_publish_config.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_publish_pipeline.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_server.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_sync_anchoring.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_sync_command.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_sync_comments.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_sync_github.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_sync_state.py +0 -0
- {mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_treeutil.py +0 -0
{mkdocs2confluence-0.18.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.19.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.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
|
|
@@ -28,14 +28,14 @@ Requires-Dist: httpx>=0.28
|
|
|
28
28
|
Requires-Dist: idna>=3.18
|
|
29
29
|
Requires-Dist: tinycss2>=1.5.1
|
|
30
30
|
Provides-Extra: pdf
|
|
31
|
-
Requires-Dist: weasyprint>=
|
|
31
|
+
Requires-Dist: weasyprint>=70.0; extra == "pdf"
|
|
32
32
|
Provides-Extra: dev
|
|
33
33
|
Requires-Dist: pytest; extra == "dev"
|
|
34
34
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
35
35
|
Requires-Dist: ruff; extra == "dev"
|
|
36
36
|
Requires-Dist: mypy; extra == "dev"
|
|
37
37
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
38
|
-
Requires-Dist: bandit; extra == "dev"
|
|
38
|
+
Requires-Dist: bandit[sarif]; extra == "dev"
|
|
39
39
|
Requires-Dist: build; extra == "dev"
|
|
40
40
|
Requires-Dist: pre-commit; extra == "dev"
|
|
41
41
|
Dynamic: license-file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mkdocs2confluence"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.19.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" }
|
|
@@ -44,7 +44,7 @@ Changelog = "https://github.com/jeckyl2010/mkdocs2confluence/releases"
|
|
|
44
44
|
|
|
45
45
|
[project.optional-dependencies]
|
|
46
46
|
pdf = [
|
|
47
|
-
"weasyprint>=
|
|
47
|
+
"weasyprint>=70.0",
|
|
48
48
|
]
|
|
49
49
|
dev = [
|
|
50
50
|
"pytest",
|
|
@@ -52,7 +52,7 @@ dev = [
|
|
|
52
52
|
"ruff",
|
|
53
53
|
"mypy",
|
|
54
54
|
"types-PyYAML",
|
|
55
|
-
"bandit",
|
|
55
|
+
"bandit[sarif]",
|
|
56
56
|
"build",
|
|
57
57
|
"pre-commit",
|
|
58
58
|
]
|
|
@@ -74,7 +74,22 @@ where = ["src"]
|
|
|
74
74
|
line-length = 120
|
|
75
75
|
|
|
76
76
|
[tool.ruff.lint]
|
|
77
|
-
|
|
77
|
+
# S = flake8-bandit (security), B = flake8-bugbear (likely bugs).
|
|
78
|
+
select = ["E", "F", "I", "S", "B"]
|
|
79
|
+
ignore = [
|
|
80
|
+
# Deliberate throughout: best-effort side effects (content hashes, managed-page
|
|
81
|
+
# stamps) whose failure is self-healing on the next run. Each site says so.
|
|
82
|
+
"S110", # try-except-pass
|
|
83
|
+
# Fixed argument lists only — `git log` in the footer transform and the macOS
|
|
84
|
+
# DYLD self-re-exec in cli.py. No user input reaches a subprocess.
|
|
85
|
+
"S603", "S606", "S607",
|
|
86
|
+
# Retry jitter in http_retry.py, not a security context.
|
|
87
|
+
"S311",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.per-file-ignores]
|
|
91
|
+
# Tests assert freely and use throwaway credentials in fixtures.
|
|
92
|
+
"tests/**" = ["S101", "S105", "S106", "S107"]
|
|
78
93
|
|
|
79
94
|
[tool.mypy]
|
|
80
95
|
strict = true
|
|
@@ -104,7 +119,7 @@ exclude_lines = [
|
|
|
104
119
|
]
|
|
105
120
|
|
|
106
121
|
# Architecture boundaries — enforces the compile pipeline stage separation
|
|
107
|
-
# documented in .
|
|
122
|
+
# documented in AGENTS.md. Higher layers may import lower
|
|
108
123
|
# ones; siblings joined by "|" must stay independent of each other.
|
|
109
124
|
[tool.importlinter]
|
|
110
125
|
root_package = "mkdocs_to_confluence"
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0/src/mkdocs2confluence.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.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
|
|
@@ -28,14 +28,14 @@ Requires-Dist: httpx>=0.28
|
|
|
28
28
|
Requires-Dist: idna>=3.18
|
|
29
29
|
Requires-Dist: tinycss2>=1.5.1
|
|
30
30
|
Provides-Extra: pdf
|
|
31
|
-
Requires-Dist: weasyprint>=
|
|
31
|
+
Requires-Dist: weasyprint>=70.0; extra == "pdf"
|
|
32
32
|
Provides-Extra: dev
|
|
33
33
|
Requires-Dist: pytest; extra == "dev"
|
|
34
34
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
35
35
|
Requires-Dist: ruff; extra == "dev"
|
|
36
36
|
Requires-Dist: mypy; extra == "dev"
|
|
37
37
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
38
|
-
Requires-Dist: bandit; extra == "dev"
|
|
38
|
+
Requires-Dist: bandit[sarif]; extra == "dev"
|
|
39
39
|
Requires-Dist: build; extra == "dev"
|
|
40
40
|
Requires-Dist: pre-commit; extra == "dev"
|
|
41
41
|
Dynamic: license-file
|
|
@@ -274,7 +274,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
274
274
|
"Detected targets (all installed when no --tool given):\n"
|
|
275
275
|
" hermes ~/.hermes/skills/tooling/mkdocs-changelog/SKILL.md\n"
|
|
276
276
|
" github-skills .github/skills/tooling/mkdocs-changelog/SKILL.md\n"
|
|
277
|
-
" claude .claude/commands/changelog.md (frontmatter stripped)\n"
|
|
277
|
+
" claude .claude/commands/mk2conf-changelog.md (frontmatter stripped)\n"
|
|
278
278
|
" copilot .github/instructions/mk2conf-changelog.instructions.md\n"
|
|
279
279
|
" cursor .cursor/rules/mk2conf-changelog.mdc\n"
|
|
280
280
|
"\n"
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/config.py
RENAMED
|
@@ -154,7 +154,11 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
|
|
|
154
154
|
raise FileNotFoundError(f"mkdocs.yml not found: {mkdocs_yml}")
|
|
155
155
|
|
|
156
156
|
with mkdocs_yml.open(encoding="utf-8") as fh:
|
|
157
|
-
|
|
157
|
+
# B506 is suppressed below because _make_env_loader() returns a
|
|
158
|
+
# yaml.SafeLoader subclass — this is a safe load. Bandit cannot resolve the
|
|
159
|
+
# loader through the function call, so it assumes the worst. Removing the
|
|
160
|
+
# suppression reintroduces a medium-severity finding that fails CI.
|
|
161
|
+
raw: object = yaml.load(fh, Loader=_make_env_loader()) # noqa: S506 # nosec B506
|
|
158
162
|
|
|
159
163
|
if not isinstance(raw, dict):
|
|
160
164
|
raise ConfigError("mkdocs.yml must be a YAML mapping at the top level.")
|
|
@@ -276,7 +280,7 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
|
|
|
276
280
|
raise ConfigError(
|
|
277
281
|
f"mkdocs.yml: 'confluence.changelog' path {cl_str!r} "
|
|
278
282
|
"escapes docs_dir. The path must be relative to the docs directory."
|
|
279
|
-
)
|
|
283
|
+
) from None
|
|
280
284
|
changelog_file = cl_str
|
|
281
285
|
|
|
282
286
|
# exclude_properties (optional) — raw front matter keys to omit from
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/parser/markdown.py
RENAMED
|
@@ -256,7 +256,7 @@ _FENCE_OPEN_RE = re.compile(r"^(?P<fence>`{3,}|~{3,})(?P<info>.*)$")
|
|
|
256
256
|
|
|
257
257
|
# Matches an admonition opener: !!! kind / !!! kind "title" / ??? kind / ???+ kind
|
|
258
258
|
_ADMONITION_RE = re.compile(
|
|
259
|
-
r"""^(?P<marker>!{3}|\?{3}\+?)\s+(?P<kind>\w+)(?:\s+(?P<q>["'])(?P<title>.*?)(?P=q))
|
|
259
|
+
r"""^[ ]{0,3}(?P<marker>!{3}|\?{3}\+?)\s+(?P<kind>\w+)(?:\s+(?P<q>["'])(?P<title>.*?)(?P=q))?\s*$"""
|
|
260
260
|
)
|
|
261
261
|
|
|
262
262
|
# Matches a Material for MkDocs content tab opener: === "Label"
|
|
@@ -296,39 +296,129 @@ _FOOTNOTE_REF_RE = re.compile(r'^\[\^(?P<label>[^\]]+)\]')
|
|
|
296
296
|
# Matches a definition list definition line: : text
|
|
297
297
|
_DEFLIST_DEF_RE = re.compile(r'^:\s+(?P<text>.+)$')
|
|
298
298
|
|
|
299
|
-
# Matches <div class="
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
# Matches any <div ... class="..."> opener, capturing the class list. Material has
|
|
300
|
+
# two grid flavours: ``grid cards`` (each list item / admonition is a card) and a
|
|
301
|
+
# plain ``grid`` (each top-level block is one grid item).
|
|
302
|
+
_DIV_CLASS_RE = re.compile(
|
|
303
|
+
r'^\s*<div\b[^>]*\bclass=["\'](?P<classes>[^"\']*)["\'][^>]*>\s*$',
|
|
302
304
|
re.IGNORECASE,
|
|
303
305
|
)
|
|
306
|
+
# Any <div> opener, used only for nesting depth so an inner div cannot close the grid.
|
|
307
|
+
_ANY_DIV_OPEN_RE = re.compile(r'^\s*<div\b[^>]*>\s*$', re.IGNORECASE)
|
|
304
308
|
_CLOSE_DIV_RE = re.compile(r'^\s*</div>\s*$', re.IGNORECASE)
|
|
305
309
|
|
|
310
|
+
# An attr-list line opting a block into card styling: { .card }
|
|
311
|
+
_CARD_ATTR_RE = re.compile(r'^\s*\{:?\s*\.card\b[^}]*\}\s*$')
|
|
306
312
|
|
|
307
|
-
def _tokenize_grid_cards(inner_lines: list[str]) -> _GridCardsToken:
|
|
308
|
-
"""Convert the inner lines of a grid cards div into a ``_GridCardsToken``.
|
|
309
313
|
|
|
310
|
-
|
|
314
|
+
def _grid_kind(line: str) -> str | None:
|
|
315
|
+
"""Return ``"cards"``, ``"grid"``, or ``None`` for a ``<div>`` opener line.
|
|
311
316
|
|
|
312
|
-
|
|
317
|
+
``grid cards`` wins over ``grid``; a div without a ``grid`` class is not ours.
|
|
318
|
+
"""
|
|
319
|
+
m = _DIV_CLASS_RE.match(line)
|
|
320
|
+
if m is None:
|
|
321
|
+
return None
|
|
322
|
+
classes = m.group("classes").split()
|
|
323
|
+
if "grid" not in classes:
|
|
324
|
+
return None
|
|
325
|
+
return "cards" if "cards" in classes else "grid"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Matches a top-level card bullet (no leading indent): ``- ``, ``* ``, ``+ ``.
|
|
329
|
+
_CARD_BULLET_RE = re.compile(r'^(?P<marker>[-*+]\s+)(?P<text>.*)$')
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _expand_leading_tabs(line: str) -> str:
|
|
333
|
+
"""Expand tabs in *line*'s leading whitespace to 4-space tab stops.
|
|
334
|
+
|
|
335
|
+
Indent width drives every block decision below, and Markdown counts a tab as
|
|
336
|
+
4 columns. Only the indent is touched — tabs inside code or text are content.
|
|
337
|
+
"""
|
|
338
|
+
stripped = line.lstrip(" \t")
|
|
339
|
+
indent = line[: len(line) - len(stripped)]
|
|
340
|
+
return indent.expandtabs(4) + stripped
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _split_card_blocks(lines: list[str]) -> list[list[str]] | None:
|
|
344
|
+
"""Split *lines* into one block of lines per top-level bullet.
|
|
345
|
+
|
|
346
|
+
The bullet marker is removed from the first line of each block and its
|
|
347
|
+
continuation lines are dedented by the marker width, so each block reads as
|
|
348
|
+
standalone Markdown. Returns ``None`` when there are no top-level bullets,
|
|
349
|
+
leaving the caller on its non-bullet path.
|
|
350
|
+
"""
|
|
351
|
+
starts = [
|
|
352
|
+
(i, m) for i, ln in enumerate(lines) if (m := _CARD_BULLET_RE.match(ln)) is not None
|
|
353
|
+
]
|
|
354
|
+
if not starts:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
blocks: list[list[str]] = []
|
|
358
|
+
for n, (start, m) in enumerate(starts):
|
|
359
|
+
end = starts[n + 1][0] if n + 1 < len(starts) else len(lines)
|
|
360
|
+
dedent = len(m.group("marker"))
|
|
361
|
+
body: list[str] = []
|
|
362
|
+
for raw in lines[start + 1:end]:
|
|
363
|
+
ln = _expand_leading_tabs(raw)
|
|
364
|
+
# Only strip indentation that is actually whitespace; a line that is
|
|
365
|
+
# under-indented keeps whatever it has rather than losing content.
|
|
366
|
+
body.append(ln[dedent:] if ln[:dedent].isspace() else ln)
|
|
367
|
+
blocks.append([m.group("text"), *_strip_insignificant_indent(body)])
|
|
368
|
+
return blocks
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _strip_insignificant_indent(lines: list[str]) -> list[str]:
|
|
372
|
+
"""Remove a common leading indent of 1-3 spaces from *lines*.
|
|
373
|
+
|
|
374
|
+
Authors routinely indent card bodies by 4 while the list marker is only 2
|
|
375
|
+
wide, leaving 2 stray spaces after the marker dedent. Markdown treats fewer
|
|
376
|
+
than 4 spaces as insignificant, but our block patterns anchor at ``^``, so
|
|
377
|
+
those strays would stop a ``---``, a code fence or a heading from being
|
|
378
|
+
recognised. An indent of 4+ is left alone: that is what delimits an
|
|
379
|
+
admonition body, and removing it would detach the body from its opener.
|
|
380
|
+
"""
|
|
381
|
+
indents = [len(ln) - len(ln.lstrip()) for ln in lines if ln.strip()]
|
|
382
|
+
common = min(indents, default=0)
|
|
383
|
+
if not 0 < common < 4:
|
|
384
|
+
return lines
|
|
385
|
+
return [ln[common:] if ln.strip() else ln for ln in lines]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _tokenize_grid(inner_lines: list[str], *, kind: str) -> _GridCardsToken:
|
|
389
|
+
"""Convert the inner lines of a Material grid div into a ``_GridCardsToken``.
|
|
390
|
+
|
|
391
|
+
*kind* is ``"cards"`` for ``<div class="grid cards">`` or ``"grid"`` for a
|
|
392
|
+
plain ``<div class="grid">``. Both map onto the same Confluence layout; they
|
|
393
|
+
differ only in what counts as one item:
|
|
394
|
+
|
|
395
|
+
* ``cards`` — each top-level bullet is a card, and the bullet marker is
|
|
396
|
+
stripped so the item's own block content (admonition, nested list, code
|
|
397
|
+
block) parses normally::
|
|
313
398
|
|
|
314
399
|
- :icon: **Title** — description text
|
|
400
|
+
- !!! tip "Title"
|
|
315
401
|
|
|
316
|
-
|
|
402
|
+
With no bullets present, each top-level block is a card instead, which is
|
|
403
|
+
the admonition-list form::
|
|
317
404
|
|
|
318
405
|
!!! tip "Title"
|
|
319
406
|
Body content
|
|
407
|
+
|
|
408
|
+
* ``grid`` — each top-level block is one grid item and keeps its own
|
|
409
|
+
rendering; a bullet list is a single item, not one item per bullet.
|
|
410
|
+
``{ .card }`` styling hints are dropped, having no Confluence equivalent.
|
|
320
411
|
"""
|
|
321
|
-
|
|
412
|
+
if kind == "cards":
|
|
413
|
+
blocks = _split_card_blocks(inner_lines)
|
|
414
|
+
if blocks is not None:
|
|
415
|
+
return _GridCardsToken(cards=[_tokenize("\n".join(b)) for b in blocks])
|
|
416
|
+
else:
|
|
417
|
+
inner_lines = [ln for ln in inner_lines if not _CARD_ATTR_RE.match(ln)]
|
|
322
418
|
|
|
323
|
-
|
|
324
|
-
if len(inner_tokens) == 1 and isinstance(inner_tokens[0], _BulletListToken):
|
|
325
|
-
cards: list[list[_Token]] = [
|
|
326
|
-
[_ParagraphToken(lines=[item.text])]
|
|
327
|
-
for item in inner_tokens[0].items
|
|
328
|
-
]
|
|
329
|
-
return _GridCardsToken(cards=cards)
|
|
419
|
+
inner_tokens = _tokenize("\n".join(inner_lines))
|
|
330
420
|
|
|
331
|
-
#
|
|
421
|
+
# Each top-level token is one item.
|
|
332
422
|
return _GridCardsToken(cards=[[tok] for tok in inner_tokens])
|
|
333
423
|
|
|
334
424
|
|
|
@@ -344,13 +434,15 @@ def _tokenize(text: str) -> list[_Token]:
|
|
|
344
434
|
while i < len(lines):
|
|
345
435
|
line = lines[i]
|
|
346
436
|
|
|
347
|
-
# ── Grid cards
|
|
348
|
-
|
|
437
|
+
# ── Grid div (``grid cards`` or plain ``grid``) ──────────────────────
|
|
438
|
+
grid_kind = _grid_kind(line)
|
|
439
|
+
if grid_kind is not None:
|
|
349
440
|
i += 1
|
|
350
441
|
inner_lines: list[str] = []
|
|
351
442
|
depth = 1
|
|
352
443
|
while i < len(lines) and depth > 0:
|
|
353
|
-
|
|
444
|
+
# Any nested <div> must be counted, or its </div> closes the grid early.
|
|
445
|
+
if _ANY_DIV_OPEN_RE.match(lines[i]):
|
|
354
446
|
depth += 1
|
|
355
447
|
elif _CLOSE_DIV_RE.match(lines[i]):
|
|
356
448
|
depth -= 1
|
|
@@ -360,7 +452,7 @@ def _tokenize(text: str) -> list[_Token]:
|
|
|
360
452
|
if depth > 0:
|
|
361
453
|
inner_lines.append(lines[i])
|
|
362
454
|
i += 1
|
|
363
|
-
tokens.append(
|
|
455
|
+
tokens.append(_tokenize_grid(inner_lines, kind=grid_kind))
|
|
364
456
|
continue
|
|
365
457
|
|
|
366
458
|
# ── Fenced code block ────────────────────────────────────────────────
|
|
@@ -1207,7 +1299,9 @@ def _build_tree(
|
|
|
1207
1299
|
def _make_cell(
|
|
1208
1300
|
text: str, col: int, is_header: bool = False
|
|
1209
1301
|
) -> TableCell:
|
|
1210
|
-
|
|
1302
|
+
# B023 is suppressed below: _make_cell is defined and fully consumed
|
|
1303
|
+
# inside a single loop iteration, so aligns cannot be rebound under it.
|
|
1304
|
+
align = aligns[col] if col < len(aligns) else None # noqa: B023
|
|
1211
1305
|
return TableCell(
|
|
1212
1306
|
children=_parse_inline(text, fn_map=_fn),
|
|
1213
1307
|
align=align,
|
|
@@ -294,7 +294,7 @@ def _parse_spec(
|
|
|
294
294
|
raise IncludeError(
|
|
295
295
|
f"{source_path}:{lineno}: named-section includes are not yet "
|
|
296
296
|
f"supported: {raw_spec!r}"
|
|
297
|
-
)
|
|
297
|
+
) from None
|
|
298
298
|
if start < 1 or end < start:
|
|
299
299
|
raise IncludeError(
|
|
300
300
|
f"{source_path}:{lineno}: invalid line range {start}:{end} "
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/client.py
RENAMED
|
@@ -7,6 +7,7 @@ Authentication is HTTP Basic with email + API token.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import base64
|
|
10
|
+
from functools import partial
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any, cast
|
|
12
13
|
from urllib.parse import parse_qs, urlparse
|
|
@@ -378,7 +379,9 @@ class ConfluenceClient:
|
|
|
378
379
|
name = lbl.get("name", "")
|
|
379
380
|
if name:
|
|
380
381
|
http_request_with_retry(
|
|
381
|
-
|
|
382
|
+
# partial binds name now; a closure would late-bind and could
|
|
383
|
+
# delete the wrong label if the helper ever defers the call.
|
|
384
|
+
partial(self._http.delete, label_url, params={"name": name}),
|
|
382
385
|
f"set_page_labels({page_id!r})",
|
|
383
386
|
)
|
|
384
387
|
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/skill_installer.py
RENAMED
|
@@ -90,13 +90,13 @@ def install_skill(
|
|
|
90
90
|
dest.write_text(content_body, encoding="utf-8")
|
|
91
91
|
installed.append(("claude", dest))
|
|
92
92
|
|
|
93
|
-
# GitHub Copilot —
|
|
93
|
+
# GitHub Copilot — .instructions.md needs applyTo frontmatter or Copilot ignores it
|
|
94
94
|
if _want("copilot"):
|
|
95
95
|
copilot_marker = project_dir / ".github" / "copilot-instructions.md"
|
|
96
96
|
if explicit or copilot_marker.exists():
|
|
97
97
|
dest = project_dir / ".github" / "instructions" / "mk2conf-changelog.instructions.md"
|
|
98
98
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
-
dest.write_text(content_body, encoding="utf-8")
|
|
99
|
+
dest.write_text(f'---\napplyTo: "**"\n---\n\n{content_body}', encoding="utf-8")
|
|
100
100
|
installed.append(("copilot", dest))
|
|
101
101
|
|
|
102
102
|
# Cursor — project-level, body as .mdc
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/_kroki.py
RENAMED
|
@@ -53,7 +53,10 @@ def kroki_post(source: str, diagram_type: str, kroki_url: str, fmt: str = "png")
|
|
|
53
53
|
"""POST *source* to Kroki for *diagram_type* and return rendered bytes."""
|
|
54
54
|
url = f"{kroki_url.rstrip('/')}/{diagram_type}/{fmt}"
|
|
55
55
|
body = source.encode("utf-8")
|
|
56
|
-
|
|
56
|
+
# kroki_url comes from config, so the scheme is not attacker-controlled, but it is
|
|
57
|
+
# also not validated. Switching this to httpx (already a dependency) would restrict
|
|
58
|
+
# it to http/https and remove the urllib import entirely.
|
|
59
|
+
req = urllib.request.Request( # noqa: S310
|
|
57
60
|
url,
|
|
58
61
|
data=body,
|
|
59
62
|
headers={
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/mermaid.py
RENAMED
|
@@ -62,7 +62,7 @@ def _mermaid_ink_png(source: str) -> bytes:
|
|
|
62
62
|
compressed = zlib.compress(payload.encode("utf-8"))
|
|
63
63
|
encoded = base64.urlsafe_b64encode(compressed).decode().rstrip("=")
|
|
64
64
|
url = f"{_MERMAID_INK_URL}/img/pako:{encoded}?type=png"
|
|
65
|
-
req = urllib.request.Request(url, headers={"User-Agent": "mk2conf/1.0"})
|
|
65
|
+
req = urllib.request.Request(url, headers={"User-Agent": "mk2conf/1.0"}) # noqa: S310
|
|
66
66
|
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 # nosec B310
|
|
67
67
|
return cast(bytes, resp.read())
|
|
68
68
|
|
|
@@ -1460,3 +1460,211 @@ class TestAnchorNode:
|
|
|
1460
1460
|
assert '<ac:link ac:anchor="section-1">' in xhtml
|
|
1461
1461
|
assert 'ac:structured-macro ac:name="anchor"' in xhtml
|
|
1462
1462
|
assert "section-1" in xhtml
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
class TestAdmonitionOpenerWhitespace:
|
|
1466
|
+
"""Opener lines survive trailing whitespace and <=3 spaces of indent."""
|
|
1467
|
+
|
|
1468
|
+
@pytest.mark.parametrize("opener", [
|
|
1469
|
+
'!!! abstract "Description"',
|
|
1470
|
+
'!!! abstract "Description" ', # trailing space
|
|
1471
|
+
' !!! abstract "Description"', # 3-space indent
|
|
1472
|
+
'??? tip "T" ',
|
|
1473
|
+
])
|
|
1474
|
+
def test_opener_variants_parse(self, opener: str) -> None:
|
|
1475
|
+
nodes = parse(f"{opener}\n\n Body text.\n")
|
|
1476
|
+
assert isinstance(nodes[0], Admonition), f"failed for {opener!r}"
|
|
1477
|
+
|
|
1478
|
+
def test_four_space_indent_is_not_an_admonition(self) -> None:
|
|
1479
|
+
"""4 spaces is an indented code block, not an opener."""
|
|
1480
|
+
nodes = parse("text\n\n !!! abstract \"x\"\n")
|
|
1481
|
+
assert not any(isinstance(n, Admonition) for n in nodes)
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
class TestGridCardAdmonitions:
|
|
1485
|
+
"""`- !!! kind "Title"` inside grid cards is a card, not raw paragraph text."""
|
|
1486
|
+
|
|
1487
|
+
DIV = '<div class="grid cards" markdown>\n\n'
|
|
1488
|
+
END = '\n</div>\n'
|
|
1489
|
+
|
|
1490
|
+
def _emit(self, body: str) -> str:
|
|
1491
|
+
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
1492
|
+
return emit(parse(self.DIV + body + self.END))
|
|
1493
|
+
|
|
1494
|
+
def test_bulleted_admonitions_become_admonition_cards(self) -> None:
|
|
1495
|
+
xhtml = self._emit(
|
|
1496
|
+
'- !!! abstract "Description"\n This is a test\n\n'
|
|
1497
|
+
'- !!! note "Test me"\n This is hello\n'
|
|
1498
|
+
)
|
|
1499
|
+
assert "!!!" not in xhtml, "admonition opener leaked as raw text"
|
|
1500
|
+
assert xhtml.count('ac:structured-macro ac:name="info"') == 2
|
|
1501
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1502
|
+
assert "This is a test" in xhtml and "This is hello" in xhtml
|
|
1503
|
+
|
|
1504
|
+
def test_card_body_may_contain_a_bullet_list(self) -> None:
|
|
1505
|
+
"""A nested list inside a card must not disable admonition detection."""
|
|
1506
|
+
xhtml = self._emit('- !!! tip "T"\n\n - one\n - two\n')
|
|
1507
|
+
assert "!!!" not in xhtml
|
|
1508
|
+
assert 'ac:structured-macro ac:name="tip"' in xhtml
|
|
1509
|
+
assert "<li><p>one</p></li>" in xhtml
|
|
1510
|
+
|
|
1511
|
+
def test_icon_cards_and_admonition_cards_can_mix(self) -> None:
|
|
1512
|
+
xhtml = self._emit('- :material-cog: **Setup**\n\n- !!! tip "T"\n\n Body.\n')
|
|
1513
|
+
assert "!!!" not in xhtml
|
|
1514
|
+
assert "<strong>Setup</strong>" in xhtml
|
|
1515
|
+
assert 'ac:structured-macro ac:name="tip"' in xhtml
|
|
1516
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1517
|
+
|
|
1518
|
+
def test_card_body_may_contain_a_code_block(self) -> None:
|
|
1519
|
+
xhtml = self._emit('- !!! tip "T"\n\n ```python\n x = 1\n ```\n')
|
|
1520
|
+
assert 'ac:name="code"' in xhtml
|
|
1521
|
+
assert "x = 1" in xhtml
|
|
1522
|
+
|
|
1523
|
+
def test_icon_cards_are_left_alone(self) -> None:
|
|
1524
|
+
xhtml = self._emit('- :material-cog: **Setup**\n- :material-run: **Run**\n')
|
|
1525
|
+
assert "ac:structured-macro" not in xhtml
|
|
1526
|
+
assert "<strong>Setup</strong>" in xhtml
|
|
1527
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1528
|
+
|
|
1529
|
+
def test_icon_card_keeps_its_description_paragraph(self) -> None:
|
|
1530
|
+
xhtml = self._emit('- :material-cog: **Setup**\n\n Description text.\n')
|
|
1531
|
+
assert "Description text." in xhtml
|
|
1532
|
+
|
|
1533
|
+
def test_non_admonition_triple_marker_stays_literal(self) -> None:
|
|
1534
|
+
"""`!?!` is not an admonition marker and must render as text."""
|
|
1535
|
+
assert "!?!" in self._emit('- !?! not an admonition\n')
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
class TestGenericGrid:
|
|
1539
|
+
"""`<div class="grid">` — each top-level block is one grid item."""
|
|
1540
|
+
|
|
1541
|
+
def _emit(self, src: str) -> str:
|
|
1542
|
+
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
1543
|
+
return emit(parse(src))
|
|
1544
|
+
|
|
1545
|
+
def _grid(self, body: str, classes: str = "grid") -> str:
|
|
1546
|
+
return f'<div class="{classes}" markdown>\n\n{body}\n</div>\n'
|
|
1547
|
+
|
|
1548
|
+
def test_admonitions_become_grid_items(self) -> None:
|
|
1549
|
+
xhtml = self._emit(self._grid(
|
|
1550
|
+
'!!! tip "A"\n\n Body A\n\n!!! note "B"\n\n Body B\n'
|
|
1551
|
+
))
|
|
1552
|
+
assert "<div" not in xhtml, "grid div leaked as literal text"
|
|
1553
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1554
|
+
assert 'ac:structured-macro ac:name="tip"' in xhtml
|
|
1555
|
+
|
|
1556
|
+
def test_bullet_list_is_one_item_not_one_per_bullet(self) -> None:
|
|
1557
|
+
"""The `cards` card-splitting must not apply to a plain grid."""
|
|
1558
|
+
xhtml = self._emit(self._grid("- one\n- two\n"))
|
|
1559
|
+
assert xhtml.count("<ac:layout-cell>") == 1
|
|
1560
|
+
assert "<li><p>one</p></li>" in xhtml
|
|
1561
|
+
|
|
1562
|
+
def test_card_attr_list_is_dropped(self) -> None:
|
|
1563
|
+
"""`{ .card }` is a styling hint with no Confluence equivalent."""
|
|
1564
|
+
xhtml = self._emit(self._grid(
|
|
1565
|
+
"**HTML** for structure\n{ .card }\n\n**JS** for logic\n{ .card }\n"
|
|
1566
|
+
))
|
|
1567
|
+
assert ".card" not in xhtml
|
|
1568
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1569
|
+
|
|
1570
|
+
def test_grid_cards_still_splits_on_bullets(self) -> None:
|
|
1571
|
+
"""Regression guard: `grid cards` keeps one-card-per-bullet."""
|
|
1572
|
+
xhtml = self._emit(self._grid("- one\n- two\n", classes="grid cards"))
|
|
1573
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1574
|
+
|
|
1575
|
+
def test_class_order_does_not_matter(self) -> None:
|
|
1576
|
+
xhtml = self._emit(self._grid("- one\n- two\n", classes="cards grid"))
|
|
1577
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1578
|
+
|
|
1579
|
+
def test_extra_classes_are_tolerated(self) -> None:
|
|
1580
|
+
xhtml = self._emit(self._grid("- one\n- two\n", classes="grid cards custom"))
|
|
1581
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1582
|
+
|
|
1583
|
+
def test_div_without_grid_class_is_not_a_grid(self) -> None:
|
|
1584
|
+
xhtml = self._emit('<div class="note" markdown>\n\ntext\n\n</div>\n')
|
|
1585
|
+
assert "ac:layout" not in xhtml
|
|
1586
|
+
|
|
1587
|
+
def test_nested_div_does_not_close_the_grid_early(self) -> None:
|
|
1588
|
+
"""An inner </div> must not terminate the grid and strand later items."""
|
|
1589
|
+
xhtml = self._emit(self._grid(
|
|
1590
|
+
'<div class="x">\n</div>\n\n!!! tip "A"\n\n Body\n'
|
|
1591
|
+
))
|
|
1592
|
+
assert 'ac:structured-macro ac:name="tip"' in xhtml, "item after nested div was lost"
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
class TestGridCardBodyBlocks:
|
|
1596
|
+
"""Card bodies keep block structure; a 2-space marker dedent must not break it."""
|
|
1597
|
+
|
|
1598
|
+
def _emit(self, body: str) -> str:
|
|
1599
|
+
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
1600
|
+
return emit(parse(f'<div class="grid cards" markdown>\n\n{body}\n</div>\n'))
|
|
1601
|
+
|
|
1602
|
+
def test_separator_becomes_a_rule(self) -> None:
|
|
1603
|
+
"""Material's canonical card syntax uses `---` as a divider."""
|
|
1604
|
+
xhtml = self._emit("- **T**\n\n ---\n\n Body.\n")
|
|
1605
|
+
assert "<hr/>" in xhtml
|
|
1606
|
+
assert "<p>---</p>" not in xhtml
|
|
1607
|
+
|
|
1608
|
+
def test_code_fence_stays_a_code_block(self) -> None:
|
|
1609
|
+
xhtml = self._emit("- **T**\n\n ```python\n x = 1\n ```\n")
|
|
1610
|
+
assert 'ac:name="code"' in xhtml
|
|
1611
|
+
assert "<![CDATA[x = 1]]>" in xhtml
|
|
1612
|
+
assert "<code>python" not in xhtml, "fence collapsed into inline code"
|
|
1613
|
+
|
|
1614
|
+
def test_heading_in_card_body(self) -> None:
|
|
1615
|
+
xhtml = self._emit("- **T**\n\n ## Sub\n\n Body.\n")
|
|
1616
|
+
assert "<h2>Sub</h2>" in xhtml
|
|
1617
|
+
|
|
1618
|
+
def test_admonition_body_indent_is_preserved(self) -> None:
|
|
1619
|
+
"""4+ spaces delimit an admonition body and must survive the dedent."""
|
|
1620
|
+
xhtml = self._emit('- !!! tip "T"\n\n - one\n - two\n')
|
|
1621
|
+
assert 'ac:structured-macro ac:name="tip"' in xhtml
|
|
1622
|
+
# The list must be *inside* the panel, not a sibling after it.
|
|
1623
|
+
body = xhtml.split("<ac:rich-text-body>")[1]
|
|
1624
|
+
assert "<li><p>one</p></li>" in body.split("</ac:rich-text-body>")[0]
|
|
1625
|
+
|
|
1626
|
+
def test_material_canonical_complex_example(self) -> None:
|
|
1627
|
+
"""The example straight from the Material grids reference page."""
|
|
1628
|
+
xhtml = self._emit(
|
|
1629
|
+
"- :material-clock-fast:{ .lg .middle } __Set up in 5 minutes__\n\n"
|
|
1630
|
+
" ---\n\n"
|
|
1631
|
+
" Install it and get up and running in minutes\n\n"
|
|
1632
|
+
"- :fontawesome-brands-markdown:{ .lg .middle } __It's just Markdown__\n\n"
|
|
1633
|
+
" ---\n\n"
|
|
1634
|
+
" Focus on your content\n"
|
|
1635
|
+
)
|
|
1636
|
+
assert xhtml.count("<ac:layout-cell>") == 2
|
|
1637
|
+
assert xhtml.count("<hr/>") == 2
|
|
1638
|
+
assert "Set up in 5 minutes" in xhtml and "Focus on your content" in xhtml
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
class TestGridCardIndentEdgeCases:
|
|
1642
|
+
"""Marker width and tab indentation must not detach a card's body."""
|
|
1643
|
+
|
|
1644
|
+
def _emit(self, body: str) -> str:
|
|
1645
|
+
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
1646
|
+
return emit(parse(f'<div class="grid cards" markdown>\n\n{body}\n</div>\n'))
|
|
1647
|
+
|
|
1648
|
+
def _panel_body(self, xhtml: str) -> str:
|
|
1649
|
+
return xhtml.split("<ac:rich-text-body>")[1].split("</ac:rich-text-body>")[0]
|
|
1650
|
+
|
|
1651
|
+
def test_tab_indented_admonition_body_stays_inside_the_panel(self) -> None:
|
|
1652
|
+
xhtml = self._emit('- !!! tip "T"\n\n\t\tBody.\n')
|
|
1653
|
+
assert "Body." in self._panel_body(xhtml), "tab-indented body escaped the panel"
|
|
1654
|
+
|
|
1655
|
+
def test_tab_indented_code_fence_in_card(self) -> None:
|
|
1656
|
+
xhtml = self._emit("- **T**\n\n\t```python\n\tx = 1\n\t```\n")
|
|
1657
|
+
assert "<![CDATA[x = 1]]>" in xhtml
|
|
1658
|
+
|
|
1659
|
+
def test_wide_marker_keeps_separator(self) -> None:
|
|
1660
|
+
xhtml = self._emit("- **T**\n\n ---\n\n Body.\n")
|
|
1661
|
+
assert "<hr/>" in xhtml
|
|
1662
|
+
|
|
1663
|
+
def test_wide_marker_keeps_admonition_body(self) -> None:
|
|
1664
|
+
xhtml = self._emit('- !!! tip "T"\n\n Body.\n')
|
|
1665
|
+
assert "Body." in self._panel_body(xhtml)
|
|
1666
|
+
|
|
1667
|
+
def test_crlf_line_endings(self) -> None:
|
|
1668
|
+
src = '<div class="grid cards" markdown>\r\n\r\n- !!! tip "T"\r\n\r\n Body.\r\n\r\n</div>\r\n'
|
|
1669
|
+
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
1670
|
+
assert "Body." in self._panel_body(emit(parse(src)))
|
|
@@ -64,6 +64,8 @@ def test_install_skill_copilot(tmp_path: Path) -> None:
|
|
|
64
64
|
assert dest.name == "mk2conf-changelog.instructions.md"
|
|
65
65
|
content = dest.read_text(encoding="utf-8")
|
|
66
66
|
assert "name: mkdocs-changelog" not in content
|
|
67
|
+
# Copilot ignores .instructions.md files without applyTo frontmatter
|
|
68
|
+
assert content.startswith('---\napplyTo: "**"\n---\n\n# MkDocs Changelog Entry')
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
def test_install_skill_cursor(tmp_path: Path) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs2confluence.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs2confluence.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/compiler/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/compiler/models.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/compiler/page.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/emitter/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/emitter/xhtml.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/ir/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/ir/treeutil.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/extra_css.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/nav.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/loader/page.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/parser/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/pdf/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/pdf/generator.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/pdf/render.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/abbrevs.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/fence.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preprocess/icons.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preview/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preview/render.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/preview/server.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/executor.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/models.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/publisher/planner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/anchoring.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/command.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/comments.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/github.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/platform.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/sync/state.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/abbrevs.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/assets.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/footer.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/src/mkdocs_to_confluence/transforms/images.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.18.0 → mkdocs2confluence-0.19.0}/tests/test_exclude_properties_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|