rolfedh-doc-utils 0.1.12__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.12/rolfedh_doc_utils.egg-info → rolfedh_doc_utils-0.1.14}/PKG-INFO +1 -1
  2. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/extract_link_attributes.py +58 -25
  3. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/replace_link_attributes.py +27 -8
  4. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/extract_link_attributes.py +9 -1
  5. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/pyproject.toml +1 -1
  6. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/replace_link_attributes.py +7 -1
  7. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14/rolfedh_doc_utils.egg-info}/PKG-INFO +1 -1
  8. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/SOURCES.txt +1 -0
  9. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_extract_link_attributes.py +36 -3
  10. rolfedh_doc_utils-0.1.14/tests/test_replace_link_attributes.py +215 -0
  11. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/LICENSE +0 -0
  12. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/README.md +0 -0
  13. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/archive_unused_files.py +0 -0
  14. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/archive_unused_images.py +0 -0
  15. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/check_scannability.py +0 -0
  16. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/__init__.py +0 -0
  17. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/file_utils.py +0 -0
  18. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/format_asciidoc_spacing.py +0 -0
  19. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/scannability.py +0 -0
  20. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/spinner.py +0 -0
  21. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/topic_map_parser.py +0 -0
  22. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/unused_adoc.py +0 -0
  23. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/unused_attributes.py +0 -0
  24. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/unused_images.py +0 -0
  25. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/doc_utils/validate_links.py +0 -0
  26. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/find_unused_attributes.py +0 -0
  27. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/format_asciidoc_spacing.py +0 -0
  28. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/dependency_links.txt +0 -0
  29. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/entry_points.txt +0 -0
  30. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/requires.txt +0 -0
  31. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/rolfedh_doc_utils.egg-info/top_level.txt +0 -0
  32. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/setup.cfg +0 -0
  33. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/setup.py +0 -0
  34. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_archive_unused_files.py +0 -0
  35. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_archive_unused_images.py +0 -0
  36. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_auto_discovery.py +0 -0
  37. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_check_scannability.py +0 -0
  38. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_cli_entry_points.py +0 -0
  39. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_file_utils.py +0 -0
  40. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_fixture_archive_unused_files.py +0 -0
  41. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_fixture_archive_unused_images.py +0 -0
  42. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_fixture_check_scannability.py +0 -0
  43. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_parse_exclude_list.py +0 -0
  44. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_symlink_handling.py +0 -0
  45. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_topic_map_parser.py +0 -0
  46. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_unused_attributes.py +0 -0
  47. {rolfedh_doc_utils-0.1.12 → rolfedh_doc_utils-0.1.14}/tests/test_validate_links.py +0 -0
  48. {rolfedh_doc_utils-0.1.12 → 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.12
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
 
@@ -257,13 +268,14 @@ def collect_all_macros(scan_dirs: List[str] = None) -> List[Tuple[str, str, str,
257
268
 
258
269
  def create_attributes(url_groups: Dict[str, List[Tuple[str, str, str, int]]],
259
270
  existing_attrs: Dict[str, str],
260
- interactive: bool = True) -> Dict[str, str]:
271
+ interactive: bool = True) -> Tuple[Dict[str, str], Dict[str, str]]:
261
272
  """
262
- Create new attributes for each unique URL.
273
+ Create new attributes for each unique URL and track existing ones.
263
274
 
264
- Returns: Dict[attribute_name, attribute_value]
275
+ Returns: Tuple[new_attributes, existing_matching_attributes]
265
276
  """
266
277
  new_attributes = {}
278
+ existing_matching_attributes = {}
267
279
  existing_attr_names = set(existing_attrs.keys())
268
280
  counter = 1
269
281
 
@@ -273,6 +285,7 @@ def create_attributes(url_groups: Dict[str, List[Tuple[str, str, str, int]]],
273
285
  for attr_name, attr_value in existing_attrs.items():
274
286
  if url in attr_value:
275
287
  existing_attr = attr_name
288
+ existing_matching_attributes[attr_name] = attr_value
276
289
  break
277
290
 
278
291
  if existing_attr:
@@ -296,7 +309,7 @@ def create_attributes(url_groups: Dict[str, List[Tuple[str, str, str, int]]],
296
309
 
297
310
  print(f"Created attribute: :{attr_name}: {attr_value}")
298
311
 
299
- return new_attributes
312
+ return new_attributes, existing_matching_attributes
300
313
 
301
314
 
302
315
  def update_attribute_file(file_path: str, new_attributes: Dict[str, str], dry_run: bool = False):
@@ -450,10 +463,20 @@ def extract_link_attributes(attributes_file: str = None,
450
463
  interactive: bool = True,
451
464
  dry_run: bool = False,
452
465
  validate_links: bool = False,
453
- fail_on_broken: bool = False) -> bool:
466
+ fail_on_broken: bool = False,
467
+ macro_type: str = 'both') -> bool:
454
468
  """
455
469
  Main function to extract link attributes.
456
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
+
457
480
  Returns: True if successful, False otherwise
458
481
  """
459
482
  # Find or confirm attributes file
@@ -488,16 +511,17 @@ def extract_link_attributes(attributes_file: str = None,
488
511
  spinner.stop(f"Loaded {len(existing_attrs)} existing attributes")
489
512
 
490
513
  # Collect all macros
491
- 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")
492
516
  spinner.start()
493
- all_macros = collect_all_macros(scan_dirs)
517
+ all_macros = collect_all_macros(scan_dirs, macro_type)
494
518
  spinner.stop()
495
519
 
496
520
  if not all_macros:
497
- print("No link or xref macros with attributes found.")
521
+ print(f"No {macro_desc} macros with attributes found.")
498
522
  return True
499
523
 
500
- print(f"Found {len(all_macros)} link/xref macros with attributes")
524
+ print(f"Found {len(all_macros)} {macro_desc} macros with attributes")
501
525
 
502
526
  # Group by URL
503
527
  spinner = Spinner("Grouping macros by URL")
@@ -505,15 +529,15 @@ def extract_link_attributes(attributes_file: str = None,
505
529
  url_groups = group_macros_by_url(all_macros)
506
530
  spinner.stop(f"Grouped into {len(url_groups)} unique URLs")
507
531
 
508
- # Create new attributes
509
- new_attributes = create_attributes(url_groups, existing_attrs, interactive)
532
+ # Create new attributes and track existing ones
533
+ new_attributes, existing_matching_attributes = create_attributes(url_groups, existing_attrs, interactive)
510
534
 
511
- if not new_attributes:
512
- print("No new attributes to create.")
535
+ if not new_attributes and not existing_matching_attributes:
536
+ print("No new attributes to create and no existing attributes match found URLs.")
513
537
  return True
514
538
 
515
539
  # Validate new attributes before writing if requested
516
- if validate_links and not dry_run:
540
+ if validate_links and not dry_run and new_attributes:
517
541
  print("\nValidating new link attributes...")
518
542
  spinner = Spinner("Validating new URLs")
519
543
  spinner.start()
@@ -543,10 +567,11 @@ def extract_link_attributes(attributes_file: str = None,
543
567
  print("\nStopping due to broken URLs in new attributes (--fail-on-broken)")
544
568
  return False
545
569
 
546
- # Update attribute file
547
- update_attribute_file(attributes_file, new_attributes, dry_run)
570
+ # Update attribute file (only if there are new attributes)
571
+ if new_attributes:
572
+ update_attribute_file(attributes_file, new_attributes, dry_run)
548
573
 
549
- # Prepare file updates
574
+ # Prepare file updates (include both new and existing matching attributes)
550
575
  all_attributes = {**existing_attrs, **new_attributes}
551
576
  file_updates = prepare_file_updates(url_groups, all_attributes)
552
577
 
@@ -560,6 +585,14 @@ def extract_link_attributes(attributes_file: str = None,
560
585
  if dry_run:
561
586
  print("\n[DRY RUN] No files were modified. Run without --dry-run to apply changes.")
562
587
  else:
563
- print(f"\nSuccessfully extracted {len(new_attributes)} link attributes")
588
+ total_processed = len(new_attributes) + len(existing_matching_attributes)
589
+ if new_attributes and existing_matching_attributes:
590
+ print(f"\nSuccessfully processed {total_processed} link attributes:")
591
+ print(f" - Created {len(new_attributes)} new attributes")
592
+ print(f" - Replaced macros using {len(existing_matching_attributes)} existing attributes")
593
+ elif new_attributes:
594
+ print(f"\nSuccessfully extracted {len(new_attributes)} link attributes")
595
+ elif existing_matching_attributes:
596
+ print(f"\nSuccessfully replaced macros using {len(existing_matching_attributes)} existing link attributes")
564
597
 
565
598
  return True
@@ -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.12"
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.12
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."""
@@ -217,9 +247,10 @@ class TestCreateAttributes:
217
247
  }
218
248
  existing_attrs = {}
219
249
 
220
- new_attrs = create_attributes(url_groups, existing_attrs, interactive=False)
250
+ new_attrs, existing_matched = create_attributes(url_groups, existing_attrs, interactive=False)
221
251
 
222
252
  assert len(new_attrs) == 2
253
+ assert len(existing_matched) == 0
223
254
  # Check that attributes were created
224
255
  attr_values = list(new_attrs.values())
225
256
  assert any("link:https://example.com/{version}/guide.html[Guide]" in v for v in attr_values)
@@ -236,9 +267,11 @@ class TestCreateAttributes:
236
267
  "existing-link": "link:https://example.com/guide.html[Existing Guide]"
237
268
  }
238
269
 
239
- new_attrs = create_attributes(url_groups, existing_attrs, interactive=False)
270
+ new_attrs, existing_matched = create_attributes(url_groups, existing_attrs, interactive=False)
240
271
 
241
272
  assert len(new_attrs) == 0
273
+ assert len(existing_matched) == 1
274
+ assert "existing-link" in existing_matched
242
275
  captured = capsys.readouterr()
243
276
  assert "already has attribute" in captured.out
244
277
 
@@ -314,7 +347,7 @@ And xref:{base-url}/intro.html[Introduction] for overview.
314
347
 
315
348
  # Check output
316
349
  captured = capsys.readouterr()
317
- assert "Found 3 link/xref macros" in captured.out
350
+ assert "Found 3 link and xref macros" in captured.out
318
351
  assert "Grouped into 2 unique URLs" in captured.out
319
352
  assert "[DRY RUN]" in captured.out
320
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