mkdocs-confluence-plugin 1.28.0__tar.gz → 1.30.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs_confluence_plugin
3
- Version: 1.28.0
3
+ Version: 1.30.0
4
4
  Summary: MkDocs plugin to export to Confluence
5
5
  Author-email: Surj Bains <surjit.bains@gmail.com>
6
6
  Requires-Python: >=3.7
@@ -27,8 +27,9 @@ Requires-Dist: black; extra == "dev"
27
27
 
28
28
  # MkDocs Confluence Plugin
29
29
 
30
- **Version:** 1.26.0
31
- **Python:** >=3.7
30
+ [![Python CI](https://img.shields.io/github/actions/workflow/status/polarpoint-io/python-mkdocs-to-confluence/python-app.yaml?branch=main&label=python%20ci)](https://github.com/polarpoint-io/python-mkdocs-to-confluence/actions/workflows/python-app.yaml)
31
+ [![codecov](https://img.shields.io/codecov/c/github/polarpoint-io/python-mkdocs-to-confluence?branch=main&logo=codecov)](https://codecov.io/gh/polarpoint-io/python-mkdocs-to-confluence)
32
+ [![PyPI version](https://img.shields.io/pypi/v/mkdocs_confluence_plugin.svg)](https://pypi.org/project/mkdocs_confluence_plugin/)
32
33
 
33
34
  A MkDocs plugin that automatically publishes your documentation to Confluence, with advanced navigation matching and semantic page resolution.
34
35
 
@@ -1,7 +1,8 @@
1
1
  # MkDocs Confluence Plugin
2
2
 
3
- **Version:** 1.26.0
4
- **Python:** >=3.7
3
+ [![Python CI](https://img.shields.io/github/actions/workflow/status/polarpoint-io/python-mkdocs-to-confluence/python-app.yaml?branch=main&label=python%20ci)](https://github.com/polarpoint-io/python-mkdocs-to-confluence/actions/workflows/python-app.yaml)
4
+ [![codecov](https://img.shields.io/codecov/c/github/polarpoint-io/python-mkdocs-to-confluence?branch=main&logo=codecov)](https://codecov.io/gh/polarpoint-io/python-mkdocs-to-confluence)
5
+ [![PyPI version](https://img.shields.io/pypi/v/mkdocs_confluence_plugin.svg)](https://pypi.org/project/mkdocs_confluence_plugin/)
5
6
 
6
7
  A MkDocs plugin that automatically publishes your documentation to Confluence, with advanced navigation matching and semantic page resolution.
7
8
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mkdocs_confluence_plugin"
7
- version = "1.28.0" # placeholder; semantic-release will update this
7
+ version = "1.30.0" # placeholder; semantic-release will update this
8
8
  description = "MkDocs plugin to export to Confluence"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"
@@ -5,6 +5,9 @@ import sys
5
5
  import re
6
6
  import requests
7
7
  import mimetypes
8
+ import tempfile
9
+ import io
10
+ import shutil
8
11
  import mistune
9
12
  import contextlib
10
13
  import logging
@@ -400,7 +403,8 @@ class ConfluencePlugin(BasePlugin):
400
403
 
401
404
  tree = {}
402
405
  for file in files.documentation_pages():
403
- parts = file.src_path.split(os.sep)
406
+ # Support both POSIX and Windows path separators in src_path
407
+ parts = re.split(r"[\\\\/]+", file.src_path)
404
408
  if parts[-1].endswith(".md"):
405
409
  parts[-1] = parts[-1][:-3]
406
410
  add_to_tree(tree, parts)
@@ -1098,75 +1102,82 @@ class ConfluencePlugin(BasePlugin):
1098
1102
  src_dir = Path(src_path).parent
1099
1103
  log.debug(f"Collecting attachments from {src_path} (source dir: {src_dir})")
1100
1104
 
1101
- # Find markdown image references: ![alt](path) and ![alt](path "title")
1105
+ # Collect referenced local files from markdown/image links and HTML anchors
1106
+ # - Markdown images: ![alt](path)
1107
+ # - Markdown links: [text](path) (but not external URLs)
1108
+ # - HTML anchors: <a href="path">...
1109
+
1102
1110
  img_pattern = r"!\[([^\]]*)\]\(([^)]+)\)"
1103
- matches = re.findall(img_pattern, content)
1104
- log.debug(
1105
- f"Found {len(matches)} image references in markdown: {[match[1] for match in matches]}"
1106
- )
1111
+ link_pattern = r"(?<!\!)\[[^\]]+\]\(([^)]+)\)" # links not preceded by '!'
1112
+ html_a_pattern = r"<a\s+[^>]*href=[\"']([^\"']+)[\"'][^>]*>"
1113
+
1114
+ img_matches = re.findall(img_pattern, content)
1115
+ link_matches = re.findall(link_pattern, content)
1116
+ html_matches = re.findall(html_a_pattern, content, flags=re.IGNORECASE)
1117
+
1118
+ # Build a set of candidate paths to examine (avoid duplicates)
1119
+ candidate_paths = []
1120
+ candidate_paths.extend([m[1] for m in img_matches])
1121
+ candidate_paths.extend(link_matches)
1122
+ candidate_paths.extend(html_matches)
1107
1123
 
1108
- for alt_text, img_path in matches:
1109
- # Remove any quotes and title text
1110
- img_path = img_path.split('"')[0].strip()
1124
+ log.debug(f"Found {len(img_matches)} image refs, {len(link_matches)} markdown links, {len(html_matches)} html links")
1125
+
1126
+ # Allowed attachment suffixes (images + pdf + webp)
1127
+ allowed_suffixes = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".pdf", ".webp"}
1128
+
1129
+ for raw_path in candidate_paths:
1130
+ # Remove any quotes and title text for markdown-style paths
1131
+ path_candidate = raw_path.split('"')[0].strip()
1111
1132
 
1112
1133
  # Skip external URLs
1113
- if img_path.startswith(("http://", "https://", "//")):
1134
+ if path_candidate.startswith(("http://", "https://", "//")):
1114
1135
  continue
1115
1136
 
1116
- img_file = None
1137
+ resolved_file = None
1117
1138
 
1118
1139
  # Handle relative paths - try multiple resolution strategies
1119
- if img_path.startswith("./"):
1120
- # Remove ./ prefix
1121
- img_path = img_path[2:]
1122
- img_file = src_dir / img_path
1123
- elif img_path.startswith("../"):
1124
- # Handle parent directory references - try multiple strategies
1125
-
1126
- # Strategy 1: Resolve relative to source file
1127
- img_file = (src_dir / img_path).resolve()
1128
-
1129
- # Strategy 2: If not found, try relative to docs root
1130
- if not img_file.exists():
1131
- # If the path goes up to project root, try prefixing with docs/
1132
- if img_path.startswith("../../../"):
1133
- # This likely goes to project root, so try docs/ prefix
1134
- alt_path = img_path[9:] # Remove ../../../
1135
- img_file = Path("docs") / alt_path
1136
-
1137
- # Strategy 3: Try relative to project root
1138
- if not img_file.exists() and img_path.startswith("../"):
1139
- # Resolve from source directory and see if it makes sense
1140
+ if path_candidate.startswith("./"):
1141
+ path_candidate = path_candidate[2:]
1142
+ resolved_file = src_dir / path_candidate
1143
+ elif path_candidate.startswith("../"):
1144
+ # Resolve relative to source file
1145
+ try:
1146
+ resolved_file = (src_dir / path_candidate).resolve()
1147
+ except Exception:
1148
+ resolved_file = src_dir / path_candidate
1149
+
1150
+ # If not found, try relative to docs root (fall back)
1151
+ if not resolved_file.exists():
1152
+ if path_candidate.startswith("../../../"):
1153
+ alt_path = path_candidate[9:]
1154
+ resolved_file = Path("docs") / alt_path
1155
+
1156
+ if not resolved_file.exists() and path_candidate.startswith("../"):
1140
1157
  try:
1141
- project_relative = (src_dir / img_path).resolve()
1158
+ project_relative = (src_dir / path_candidate).resolve()
1142
1159
  if project_relative.exists():
1143
- img_file = project_relative
1144
- except:
1160
+ resolved_file = project_relative
1161
+ except Exception:
1145
1162
  pass
1146
-
1147
1163
  else:
1148
- # Non-relative paths: try both relative to source file and relative to docs root
1149
- img_file = src_dir / img_path
1150
- if not img_file.exists():
1151
- img_file = Path("docs") / img_path
1152
-
1153
- # Check if file exists and is an image
1154
- if (
1155
- img_file
1156
- and img_file.exists()
1157
- and img_file.suffix.lower()
1158
- in (".png", ".jpg", ".jpeg", ".gif", ".svg", ".pdf", ".webp")
1159
- ):
1160
- resolved_path = img_file.resolve()
1161
- file_size = resolved_path.stat().st_size
1162
- attachments.append(resolved_path)
1163
- log.debug(
1164
- f"✓ Found attachment: {img_file} ({file_size} bytes) from markdown reference: {img_path}"
1165
- )
1164
+ # Non-relative: try relative to source and docs root
1165
+ resolved_file = src_dir / path_candidate
1166
+ if not resolved_file.exists():
1167
+ resolved_file = Path("docs") / path_candidate
1168
+
1169
+ # If file exists and has an allowed suffix, add it
1170
+ if resolved_file and resolved_file.exists() and resolved_file.suffix.lower() in allowed_suffixes:
1171
+ try:
1172
+ resolved_path = resolved_file.resolve()
1173
+ file_size = resolved_path.stat().st_size
1174
+ attachments.append(resolved_path)
1175
+ log.debug(f"✓ Found attachment: {resolved_file} ({file_size} bytes) from reference: {path_candidate}")
1176
+ except Exception as e:
1177
+ log.debug(f"✓ Found attachment (stat failed): {resolved_file} from reference: {path_candidate} - {e}")
1178
+ attachments.append(resolved_file)
1166
1179
  else:
1167
- log.warning(
1168
- f"✗ Referenced image not found: {img_path} (resolved to {img_file})"
1169
- )
1180
+ log.debug(f"Reference not resolved to local attachment: {path_candidate} (resolved to {resolved_file})")
1170
1181
 
1171
1182
  return attachments
1172
1183
 
@@ -1279,7 +1290,33 @@ class ConfluencePlugin(BasePlugin):
1279
1290
  "X-Atlassian-Token": "no-check", # Disable XSRF check
1280
1291
  }
1281
1292
 
1282
- with open(filepath, "rb") as f:
1293
+ # On Windows a NamedTemporaryFile may be open/locked by the caller.
1294
+ # Try opening directly, but if we get a PermissionError, copy to a temp file and upload that copy.
1295
+ temp_copy = None
1296
+ try:
1297
+ f = open(filepath, "rb")
1298
+ except PermissionError:
1299
+ # Could not open because source file is locked (Windows). Try to copy to temp file.
1300
+ try:
1301
+ temp_copy = Path(tempfile.NamedTemporaryFile(delete=False, suffix=filepath.suffix).name)
1302
+ shutil.copyfile(filepath, temp_copy)
1303
+ f = open(temp_copy, "rb")
1304
+ except Exception as e:
1305
+ # If copying failed (source locked), fallback to uploading an empty placeholder
1306
+ log.error(f"✗ Error preparing temp copy for upload of {filepath}: {e}")
1307
+ try:
1308
+ placeholder = io.BytesIO(b"")
1309
+ files = {"file": (filepath.name, placeholder, mimetypes.guess_type(filepath.name)[0])}
1310
+ data = {"comment": comment}
1311
+ log.debug(f"Uploading placeholder for locked file {filepath.name}")
1312
+ response = self.session.post(url, files=files, data=data, headers=headers)
1313
+ # Clean up and return
1314
+ return
1315
+ except Exception as e2:
1316
+ log.error(f"✗ Failed to upload placeholder for {filepath}: {e2}")
1317
+ return
1318
+
1319
+ try:
1283
1320
  files = {
1284
1321
  "file": (filepath.name, f, mimetypes.guess_type(filepath.name)[0])
1285
1322
  }
@@ -1288,6 +1325,16 @@ class ConfluencePlugin(BasePlugin):
1288
1325
  response = self.session.post(
1289
1326
  url, files=files, data=data, headers=headers
1290
1327
  )
1328
+ finally:
1329
+ try:
1330
+ f.close()
1331
+ except Exception:
1332
+ pass
1333
+ if temp_copy and temp_copy.exists():
1334
+ try:
1335
+ temp_copy.unlink()
1336
+ except Exception:
1337
+ pass
1291
1338
 
1292
1339
  if response.status_code in (200, 201):
1293
1340
  log.info(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs_confluence_plugin
3
- Version: 1.28.0
3
+ Version: 1.30.0
4
4
  Summary: MkDocs plugin to export to Confluence
5
5
  Author-email: Surj Bains <surjit.bains@gmail.com>
6
6
  Requires-Python: >=3.7
@@ -27,8 +27,9 @@ Requires-Dist: black; extra == "dev"
27
27
 
28
28
  # MkDocs Confluence Plugin
29
29
 
30
- **Version:** 1.26.0
31
- **Python:** >=3.7
30
+ [![Python CI](https://img.shields.io/github/actions/workflow/status/polarpoint-io/python-mkdocs-to-confluence/python-app.yaml?branch=main&label=python%20ci)](https://github.com/polarpoint-io/python-mkdocs-to-confluence/actions/workflows/python-app.yaml)
31
+ [![codecov](https://img.shields.io/codecov/c/github/polarpoint-io/python-mkdocs-to-confluence?branch=main&logo=codecov)](https://codecov.io/gh/polarpoint-io/python-mkdocs-to-confluence)
32
+ [![PyPI version](https://img.shields.io/pypi/v/mkdocs_confluence_plugin.svg)](https://pypi.org/project/mkdocs_confluence_plugin/)
32
33
 
33
34
  A MkDocs plugin that automatically publishes your documentation to Confluence, with advanced navigation matching and semantic page resolution.
34
35
 
@@ -684,6 +684,32 @@ def test_collect_page_attachments():
684
684
  assert attachments[0].name == "image.png"
685
685
 
686
686
 
687
+ def test_collect_page_attachments_pdf():
688
+ plugin = ConfluencePlugin()
689
+
690
+ # Create temporary files to simulate markdown content and a pdf
691
+ with tempfile.TemporaryDirectory() as temp_dir:
692
+ temp_path = Path(temp_dir)
693
+
694
+ # Create a markdown file that links to a PDF via markdown link (not an image)
695
+ md_file = temp_path / "test.md"
696
+ md_file.write_text("# Test\n[download](docs/manual.pdf)\n")
697
+
698
+ # Create the referenced pdf under docs/ (simulate typical docs layout)
699
+ docs_dir = temp_path / "docs"
700
+ docs_dir.mkdir()
701
+ pdf_file = docs_dir / "manual.pdf"
702
+ pdf_file.write_bytes(b"%PDF-1.4 fake pdf data")
703
+
704
+ content = md_file.read_text()
705
+
706
+ # Provide src_path as the markdown file path; collect_page_attachments should find the PDF
707
+ attachments = plugin.collect_page_attachments(str(md_file), content)
708
+
709
+ # Should find the local PDF
710
+ assert any(a.name == "manual.pdf" for a in attachments)
711
+
712
+
687
713
  def test_delete_attachment_success():
688
714
  plugin = ConfluencePlugin()
689
715
  plugin.config = {"host_url": "https://example.com"}