rolfedh-doc-utils 0.1.13__tar.gz → 0.1.14__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 (48) hide show
  1. {rolfedh_doc_utils-0.1.13/rolfedh_doc_utils.egg-info → rolfedh_doc_utils-0.1.14}/PKG-INFO +1 -1
  2. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/extract_link_attributes.py +34 -12
  3. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/replace_link_attributes.py +27 -8
  4. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/extract_link_attributes.py +9 -1
  5. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/pyproject.toml +1 -1
  6. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/replace_link_attributes.py +7 -1
  7. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14/rolfedh_doc_utils.egg-info}/PKG-INFO +1 -1
  8. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/SOURCES.txt +1 -0
  9. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_extract_link_attributes.py +31 -1
  10. rolfedh_doc_utils-0.1.14/tests/test_replace_link_attributes.py +215 -0
  11. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/LICENSE +0 -0
  12. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/README.md +0 -0
  13. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/archive_unused_files.py +0 -0
  14. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/archive_unused_images.py +0 -0
  15. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/check_scannability.py +0 -0
  16. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/__init__.py +0 -0
  17. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/file_utils.py +0 -0
  18. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/format_asciidoc_spacing.py +0 -0
  19. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/scannability.py +0 -0
  20. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/spinner.py +0 -0
  21. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/topic_map_parser.py +0 -0
  22. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/unused_adoc.py +0 -0
  23. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/unused_attributes.py +0 -0
  24. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/unused_images.py +0 -0
  25. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/doc_utils/validate_links.py +0 -0
  26. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/find_unused_attributes.py +0 -0
  27. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/format_asciidoc_spacing.py +0 -0
  28. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/dependency_links.txt +0 -0
  29. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/entry_points.txt +0 -0
  30. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/requires.txt +0 -0
  31. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/top_level.txt +0 -0
  32. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/setup.cfg +0 -0
  33. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/setup.py +0 -0
  34. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_archive_unused_files.py +0 -0
  35. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_archive_unused_images.py +0 -0
  36. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_auto_discovery.py +0 -0
  37. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_check_scannability.py +0 -0
  38. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_cli_entry_points.py +0 -0
  39. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_file_utils.py +0 -0
  40. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_fixture_archive_unused_files.py +0 -0
  41. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_fixture_archive_unused_images.py +0 -0
  42. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_fixture_check_scannability.py +0 -0
  43. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_parse_exclude_list.py +0 -0
  44. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_symlink_handling.py +0 -0
  45. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_topic_map_parser.py +0 -0
  46. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_unused_attributes.py +0 -0
  47. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/tests/test_validate_links.py +0 -0
  48. {rolfedh_doc_utils-0.1.13 → rolfedh_doc_utils-0.1.14}/validate_links.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -85,10 +85,14 @@ def load_existing_attributes(file_path: str) -> Dict[str, str]:
85
85
  return attributes
86
86
 
87
87
 
88
- def find_link_macros(file_path: str) -> List[Tuple[str, str, str, int]]:
88
+ def find_link_macros(file_path: str, macro_type: str = 'both') -> List[Tuple[str, str, str, int]]:
89
89
  """
90
90
  Find all link: and xref: macros containing attributes in their URLs.
91
91
 
92
+ Args:
93
+ file_path: Path to the file to scan
94
+ macro_type: Type of macros to find - 'link', 'xref', or 'both' (default: 'both')
95
+
92
96
  Returns list of tuples: (full_macro, url, link_text, line_number)
93
97
  """
94
98
  macros = []
@@ -97,10 +101,13 @@ def find_link_macros(file_path: str) -> List[Tuple[str, str, str, int]]:
97
101
  for line_num, line in enumerate(f, 1):
98
102
  # Pattern to match link: and xref: macros
99
103
  # Matches: (link|xref):url[text] where url contains {attribute}
100
- patterns = [
101
- r'(link:([^[\]]*\{[^}]+\}[^[\]]*)\[([^\]]*)\])',
102
- r'(xref:([^[\]]*\{[^}]+\}[^[\]]*)\[([^\]]*)\])'
103
- ]
104
+ patterns = []
105
+
106
+ if macro_type in ('link', 'both'):
107
+ patterns.append(r'(link:([^[\]]*\{[^}]+\}[^[\]]*)\[([^\]]*)\])')
108
+
109
+ if macro_type in ('xref', 'both'):
110
+ patterns.append(r'(xref:([^[\]]*\{[^}]+\}[^[\]]*)\[([^\]]*)\])')
104
111
 
105
112
  for pattern in patterns:
106
113
  for match in re.finditer(pattern, line, re.IGNORECASE):
@@ -228,10 +235,14 @@ def select_link_text(url: str, variations: List[Tuple[str, str, str, int]], inte
228
235
  return most_common[0]
229
236
 
230
237
 
231
- def collect_all_macros(scan_dirs: List[str] = None) -> List[Tuple[str, str, str, str, int]]:
238
+ def collect_all_macros(scan_dirs: List[str] = None, macro_type: str = 'both') -> List[Tuple[str, str, str, str, int]]:
232
239
  """
233
240
  Collect all link/xref macros with attributes from all .adoc files.
234
241
 
242
+ Args:
243
+ scan_dirs: Directories to scan (default: current directory)
244
+ macro_type: Type of macros to find - 'link', 'xref', or 'both' (default: 'both')
245
+
235
246
  Returns: List[(file_path, full_macro, url, link_text, line_number)]
236
247
  """
237
248
  if scan_dirs is None:
@@ -248,7 +259,7 @@ def collect_all_macros(scan_dirs: List[str] = None) -> List[Tuple[str, str, str,
248
259
  for file in files:
249
260
  if file.endswith('.adoc'):
250
261
  file_path = os.path.join(root, file)
251
- macros = find_link_macros(file_path)
262
+ macros = find_link_macros(file_path, macro_type)
252
263
  for full_macro, url, link_text, line_num in macros:
253
264
  all_macros.append((file_path, full_macro, url, link_text, line_num))
254
265
 
@@ -452,10 +463,20 @@ def extract_link_attributes(attributes_file: str = None,
452
463
  interactive: bool = True,
453
464
  dry_run: bool = False,
454
465
  validate_links: bool = False,
455
- fail_on_broken: bool = False) -> bool:
466
+ fail_on_broken: bool = False,
467
+ macro_type: str = 'both') -> bool:
456
468
  """
457
469
  Main function to extract link attributes.
458
470
 
471
+ Args:
472
+ attributes_file: Path to attributes file
473
+ scan_dirs: Directories to scan
474
+ interactive: Enable interactive mode
475
+ dry_run: Preview changes without modifying files
476
+ validate_links: Validate URLs before extraction
477
+ fail_on_broken: Exit if broken links found
478
+ macro_type: Type of macros to process - 'link', 'xref', or 'both' (default: 'both')
479
+
459
480
  Returns: True if successful, False otherwise
460
481
  """
461
482
  # Find or confirm attributes file
@@ -490,16 +511,17 @@ def extract_link_attributes(attributes_file: str = None,
490
511
  spinner.stop(f"Loaded {len(existing_attrs)} existing attributes")
491
512
 
492
513
  # Collect all macros
493
- spinner = Spinner("Scanning for link and xref macros with attributes")
514
+ macro_desc = {'link': 'link', 'xref': 'xref', 'both': 'link and xref'}[macro_type]
515
+ spinner = Spinner(f"Scanning for {macro_desc} macros with attributes")
494
516
  spinner.start()
495
- all_macros = collect_all_macros(scan_dirs)
517
+ all_macros = collect_all_macros(scan_dirs, macro_type)
496
518
  spinner.stop()
497
519
 
498
520
  if not all_macros:
499
- print("No link or xref macros with attributes found.")
521
+ print(f"No {macro_desc} macros with attributes found.")
500
522
  return True
501
523
 
502
- print(f"Found {len(all_macros)} link/xref macros with attributes")
524
+ print(f"Found {len(all_macros)} {macro_desc} macros with attributes")
503
525
 
504
526
  # Group by URL
505
527
  spinner = Spinner("Grouping macros by URL")
@@ -65,8 +65,18 @@ def resolve_nested_attributes(attributes: Dict[str, str], max_iterations: int =
65
65
  return attributes
66
66
 
67
67
 
68
- def replace_link_attributes_in_file(file_path: Path, attributes: Dict[str, str], dry_run: bool = False) -> int:
69
- """Replace attribute references within link macros in a single file."""
68
+ def replace_link_attributes_in_file(file_path: Path, attributes: Dict[str, str], dry_run: bool = False, macro_type: str = 'both') -> int:
69
+ """
70
+ Replace attribute references within link macros in a single file.
71
+
72
+ Args:
73
+ file_path: Path to the file to process
74
+ attributes: Dictionary of attribute definitions
75
+ dry_run: Preview changes without modifying files
76
+ macro_type: Type of macros to process - 'link', 'xref', or 'both' (default: 'both')
77
+
78
+ Returns: Number of replacements made
79
+ """
70
80
  with open(file_path, 'r', encoding='utf-8') as f:
71
81
  content = f.read()
72
82
 
@@ -75,14 +85,23 @@ def replace_link_attributes_in_file(file_path: Path, attributes: Dict[str, str],
75
85
 
76
86
  # Find all link macros containing attributes in the URL portion only
77
87
  # Match link: and xref: macros, capturing URL and text separately
78
- link_patterns = [
88
+ link_patterns = []
89
+
90
+ if macro_type in ('link', 'both'):
79
91
  # link:url[text] - replace only in URL portion
80
- (r'link:([^[\]]*)\[([^\]]*)\]', 'link'),
92
+ link_patterns.append((r'link:([^[\]]*)\[([^\]]*)\]', 'link'))
93
+
94
+ if macro_type in ('xref', 'both'):
81
95
  # xref:target[text] - replace only in target portion
82
- (r'xref:([^[\]]*)\[([^\]]*)\]', 'xref'),
83
- # link:url[] or xref:target[] - replace in URL/target portion
84
- (r'(link|xref):([^[\]]*)\[\]', 'empty_text')
85
- ]
96
+ link_patterns.append((r'xref:([^[\]]*)\[([^\]]*)\]', 'xref'))
97
+
98
+ # Handle empty text cases based on macro type
99
+ if macro_type == 'both':
100
+ link_patterns.append((r'(link|xref):([^[\]]*)\[\]', 'empty_text'))
101
+ elif macro_type == 'link':
102
+ link_patterns.append((r'(link):([^[\]]*)\[\]', 'empty_text'))
103
+ elif macro_type == 'xref':
104
+ link_patterns.append((r'(xref):([^[\]]*)\[\]', 'empty_text'))
86
105
 
87
106
  for pattern, link_type in link_patterns:
88
107
  matches = list(re.finditer(pattern, content))
@@ -77,6 +77,13 @@ Examples:
77
77
  help='Exit extraction if broken links are found in attributes (requires --validate-links)'
78
78
  )
79
79
 
80
+ parser.add_argument(
81
+ '--macro-type',
82
+ choices=['link', 'xref', 'both'],
83
+ default='both',
84
+ help='Type of macros to process: link, xref, or both (default: both)'
85
+ )
86
+
80
87
  args = parser.parse_args()
81
88
 
82
89
  try:
@@ -86,7 +93,8 @@ Examples:
86
93
  interactive=not args.non_interactive,
87
94
  dry_run=args.dry_run,
88
95
  validate_links=args.validate_links,
89
- fail_on_broken=args.fail_on_broken
96
+ fail_on_broken=args.fail_on_broken,
97
+ macro_type=args.macro_type
90
98
  )
91
99
 
92
100
  if not success:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rolfedh-doc-utils"
7
- version = "0.1.13"
7
+ version = "0.1.14"
8
8
  description = "CLI tools for AsciiDoc documentation projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -102,6 +102,12 @@ def main():
102
102
  type=str,
103
103
  help='Path to attributes.adoc file (skips interactive selection)'
104
104
  )
105
+ parser.add_argument(
106
+ '--macro-type',
107
+ choices=['link', 'xref', 'both'],
108
+ default='both',
109
+ help='Type of macros to process: link, xref, or both (default: both)'
110
+ )
105
111
 
106
112
  args = parser.parse_args()
107
113
 
@@ -165,7 +171,7 @@ def main():
165
171
  spinner.start()
166
172
 
167
173
  for file_path in adoc_files:
168
- replacements = replace_link_attributes_in_file(file_path, attributes, args.dry_run)
174
+ replacements = replace_link_attributes_in_file(file_path, attributes, args.dry_run, args.macro_type)
169
175
  if replacements > 0:
170
176
  rel_path = file_path.relative_to(repo_root)
171
177
  total_replacements += replacements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -39,6 +39,7 @@ tests/test_fixture_archive_unused_files.py
39
39
  tests/test_fixture_archive_unused_images.py
40
40
  tests/test_fixture_check_scannability.py
41
41
  tests/test_parse_exclude_list.py
42
+ tests/test_replace_link_attributes.py
42
43
  tests/test_symlink_handling.py
43
44
  tests/test_topic_map_parser.py
44
45
  tests/test_unused_attributes.py
@@ -90,6 +90,36 @@ Regular link: link:https://example.com/guide.html[Guide]
90
90
  macros = find_link_macros(str(test_file))
91
91
  assert len(macros) == 0
92
92
 
93
+ def test_find_only_link_macros(self, tmp_path):
94
+ """Test finding only link macros when macro_type='link'."""
95
+ test_file = tmp_path / "test.adoc"
96
+ test_file.write_text("""
97
+ = Test Document
98
+
99
+ See link:https://example.com/{version}/guide.html[Guide].
100
+ Also check xref:{base-url}/intro.html[Intro].
101
+ """)
102
+
103
+ macros = find_link_macros(str(test_file), macro_type='link')
104
+
105
+ assert len(macros) == 1
106
+ assert macros[0][0] == "link:https://example.com/{version}/guide.html[Guide]"
107
+
108
+ def test_find_only_xref_macros(self, tmp_path):
109
+ """Test finding only xref macros when macro_type='xref'."""
110
+ test_file = tmp_path / "test.adoc"
111
+ test_file.write_text("""
112
+ = Test Document
113
+
114
+ See link:https://example.com/{version}/guide.html[Guide].
115
+ Also check xref:{base-url}/intro.html[Intro].
116
+ """)
117
+
118
+ macros = find_link_macros(str(test_file), macro_type='xref')
119
+
120
+ assert len(macros) == 1
121
+ assert macros[0][0] == "xref:{base-url}/intro.html[Intro]"
122
+
93
123
 
94
124
  class TestGenerateAttributeName:
95
125
  """Tests for generate_attribute_name function."""
@@ -317,7 +347,7 @@ And xref:{base-url}/intro.html[Introduction] for overview.
317
347
 
318
348
  # Check output
319
349
  captured = capsys.readouterr()
320
- assert "Found 3 link/xref macros" in captured.out
350
+ assert "Found 3 link and xref macros" in captured.out
321
351
  assert "Grouped into 2 unique URLs" in captured.out
322
352
  assert "[DRY RUN]" in captured.out
323
353
 
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for replace_link_attributes module."""
3
+
4
+ import tempfile
5
+ from pathlib import Path
6
+ import pytest
7
+ from doc_utils.replace_link_attributes import (
8
+ load_attributes,
9
+ resolve_nested_attributes,
10
+ replace_link_attributes_in_file,
11
+ )
12
+
13
+
14
+ class TestLoadAttributes:
15
+ """Tests for load_attributes function."""
16
+
17
+ def test_load_basic_attributes(self, tmp_path):
18
+ """Test loading basic attribute definitions."""
19
+ attr_file = tmp_path / "attributes.adoc"
20
+ attr_file.write_text("""
21
+ // Common attributes
22
+ :product-name: My Product
23
+ :version: 1.0
24
+ :docs-url: https://docs.example.com
25
+ """)
26
+
27
+ attrs = load_attributes(attr_file)
28
+
29
+ assert len(attrs) == 3
30
+ assert attrs["product-name"] == "My Product"
31
+ assert attrs["version"] == "1.0"
32
+ assert attrs["docs-url"] == "https://docs.example.com"
33
+
34
+
35
+ class TestResolveNestedAttributes:
36
+ """Tests for resolve_nested_attributes function."""
37
+
38
+ def test_resolve_simple_nesting(self):
39
+ """Test resolving simple nested attributes."""
40
+ attributes = {
41
+ "base-url": "https://example.com",
42
+ "docs-url": "{base-url}/docs",
43
+ "guide-url": "{docs-url}/guide.html"
44
+ }
45
+
46
+ resolved = resolve_nested_attributes(attributes)
47
+
48
+ assert resolved["base-url"] == "https://example.com"
49
+ assert resolved["docs-url"] == "https://example.com/docs"
50
+ assert resolved["guide-url"] == "https://example.com/docs/guide.html"
51
+
52
+ def test_no_nesting(self):
53
+ """Test attributes without nesting."""
54
+ attributes = {
55
+ "url1": "https://example.com",
56
+ "url2": "https://another.com"
57
+ }
58
+
59
+ resolved = resolve_nested_attributes(attributes)
60
+
61
+ assert resolved == attributes
62
+
63
+
64
+ class TestReplaceLinkAttributesInFile:
65
+ """Tests for replace_link_attributes_in_file function."""
66
+
67
+ def test_replace_in_link_macro(self, tmp_path):
68
+ """Test replacing attributes in link: macros."""
69
+ test_file = tmp_path / "test.adoc"
70
+ test_file.write_text("""
71
+ = Test Document
72
+
73
+ See link:{docs-url}/guide.html[User Guide] for details.
74
+ """)
75
+
76
+ attributes = {"docs-url": "https://docs.example.com"}
77
+
78
+ count = replace_link_attributes_in_file(test_file, attributes, dry_run=False)
79
+
80
+ assert count == 1
81
+ content = test_file.read_text()
82
+ assert "link:https://docs.example.com/guide.html[User Guide]" in content
83
+ assert "{docs-url}" not in content
84
+
85
+ def test_replace_in_xref_macro(self, tmp_path):
86
+ """Test replacing attributes in xref: macros."""
87
+ test_file = tmp_path / "test.adoc"
88
+ test_file.write_text("""
89
+ = Test Document
90
+
91
+ Check xref:{base-path}/intro.adoc[Introduction].
92
+ """)
93
+
94
+ attributes = {"base-path": "modules"}
95
+
96
+ count = replace_link_attributes_in_file(test_file, attributes, dry_run=False)
97
+
98
+ assert count == 1
99
+ content = test_file.read_text()
100
+ assert "xref:modules/intro.adoc[Introduction]" in content
101
+
102
+ def test_preserve_link_text(self, tmp_path):
103
+ """Test that link text is preserved unchanged."""
104
+ test_file = tmp_path / "test.adoc"
105
+ test_file.write_text("""
106
+ = Test
107
+
108
+ link:{url}/page.html[Custom Link Text]
109
+ """)
110
+
111
+ attributes = {"url": "https://example.com"}
112
+
113
+ replace_link_attributes_in_file(test_file, attributes, dry_run=False)
114
+
115
+ content = test_file.read_text()
116
+ assert "[Custom Link Text]" in content
117
+
118
+ def test_dry_run_no_changes(self, tmp_path):
119
+ """Test that dry-run mode doesn't modify files."""
120
+ test_file = tmp_path / "test.adoc"
121
+ original_content = """
122
+ link:{url}/page.html[Link]
123
+ """
124
+ test_file.write_text(original_content)
125
+
126
+ attributes = {"url": "https://example.com"}
127
+
128
+ replace_link_attributes_in_file(test_file, attributes, dry_run=True)
129
+
130
+ # File should be unchanged
131
+ assert test_file.read_text() == original_content
132
+
133
+ def test_macro_type_link_only(self, tmp_path):
134
+ """Test processing only link: macros."""
135
+ test_file = tmp_path / "test.adoc"
136
+ test_file.write_text("""
137
+ = Test Document
138
+
139
+ link:{docs-url}/guide.html[Guide]
140
+ xref:{base-path}/intro.adoc[Intro]
141
+ """)
142
+
143
+ attributes = {
144
+ "docs-url": "https://docs.example.com",
145
+ "base-path": "modules"
146
+ }
147
+
148
+ count = replace_link_attributes_in_file(test_file, attributes, dry_run=False, macro_type='link')
149
+
150
+ assert count == 1
151
+ content = test_file.read_text()
152
+ assert "link:https://docs.example.com/guide.html[Guide]" in content
153
+ assert "xref:{base-path}/intro.adoc[Intro]" in content # xref unchanged
154
+
155
+ def test_macro_type_xref_only(self, tmp_path):
156
+ """Test processing only xref: macros."""
157
+ test_file = tmp_path / "test.adoc"
158
+ test_file.write_text("""
159
+ = Test Document
160
+
161
+ link:{docs-url}/guide.html[Guide]
162
+ xref:{base-path}/intro.adoc[Intro]
163
+ """)
164
+
165
+ attributes = {
166
+ "docs-url": "https://docs.example.com",
167
+ "base-path": "modules"
168
+ }
169
+
170
+ count = replace_link_attributes_in_file(test_file, attributes, dry_run=False, macro_type='xref')
171
+
172
+ assert count == 1
173
+ content = test_file.read_text()
174
+ assert "link:{docs-url}/guide.html[Guide]" in content # link unchanged
175
+ assert "xref:modules/intro.adoc[Intro]" in content
176
+
177
+ def test_macro_type_both(self, tmp_path):
178
+ """Test processing both link: and xref: macros."""
179
+ test_file = tmp_path / "test.adoc"
180
+ test_file.write_text("""
181
+ = Test Document
182
+
183
+ link:{docs-url}/guide.html[Guide]
184
+ xref:{base-path}/intro.adoc[Intro]
185
+ """)
186
+
187
+ attributes = {
188
+ "docs-url": "https://docs.example.com",
189
+ "base-path": "modules"
190
+ }
191
+
192
+ count = replace_link_attributes_in_file(test_file, attributes, dry_run=False, macro_type='both')
193
+
194
+ assert count == 2
195
+ content = test_file.read_text()
196
+ assert "link:https://docs.example.com/guide.html[Guide]" in content
197
+ assert "xref:modules/intro.adoc[Intro]" in content
198
+
199
+ def test_multiple_attributes_in_url(self, tmp_path):
200
+ """Test replacing multiple attributes in a single URL."""
201
+ test_file = tmp_path / "test.adoc"
202
+ test_file.write_text("""
203
+ link:{base-url}/{version}/guide.html[Guide]
204
+ """)
205
+
206
+ attributes = {
207
+ "base-url": "https://example.com",
208
+ "version": "v2.0"
209
+ }
210
+
211
+ count = replace_link_attributes_in_file(test_file, attributes, dry_run=False)
212
+
213
+ assert count == 2
214
+ content = test_file.read_text()
215
+ assert "link:https://example.com/v2.0/guide.html[Guide]" in content