rolfedh-doc-utils 0.1.35__py3-none-any.whl → 0.1.38__py3-none-any.whl

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.
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Check Source Directives
4
+
5
+ Detects code blocks (----) that are missing [source] directive in AsciiDoc files.
6
+ This helps prevent AsciiDoc-to-DocBook XML conversion errors.
7
+
8
+ Usage:
9
+ check-source-directives # Scan current directory
10
+ check-source-directives asciidoc # Scan asciidoc/ directory
11
+ check-source-directives --fix # Scan and fix issues in current directory
12
+ check-source-directives --fix asciidoc # Scan and fix issues in asciidoc/ directory
13
+ """
14
+
15
+ import argparse
16
+ import sys
17
+ from doc_utils.missing_source_directive import find_missing_source_directives
18
+ from doc_utils.version_check import check_version_on_startup
19
+ from doc_utils.version import __version__
20
+
21
+ # ANSI color codes
22
+ RED = '\033[0;31m'
23
+ YELLOW = '\033[1;33m'
24
+ GREEN = '\033[0;32m'
25
+ NC = '\033[0m' # No Color
26
+
27
+ def main():
28
+ # Check for updates (non-blocking)
29
+ check_version_on_startup()
30
+
31
+ parser = argparse.ArgumentParser(
32
+ description='Detect code blocks (----) missing [source] directive in AsciiDoc files',
33
+ formatter_class=argparse.RawDescriptionHelpFormatter,
34
+ epilog="""
35
+ Examples:
36
+ %(prog)s # Scan current directory
37
+ %(prog)s asciidoc # Scan asciidoc/ directory
38
+ %(prog)s --fix # Scan and fix issues in current directory
39
+ %(prog)s --fix asciidoc # Scan and fix issues in asciidoc/ directory
40
+ """
41
+ )
42
+ parser.add_argument('directory', nargs='?', default='.',
43
+ help='Directory to scan (default: current directory)')
44
+ parser.add_argument('--fix', action='store_true',
45
+ help='Automatically insert [source] directives where missing')
46
+ parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
47
+
48
+ args = parser.parse_args()
49
+
50
+ mode = "Fixing" if args.fix else "Scanning for"
51
+ print(f"{mode} code blocks missing [source] directive in: {args.directory}")
52
+ print("=" * 64)
53
+ print()
54
+
55
+ try:
56
+ results = find_missing_source_directives(
57
+ scan_dir=args.directory,
58
+ auto_fix=args.fix
59
+ )
60
+ except ValueError as e:
61
+ print(f"{RED}Error: {e}{NC}", file=sys.stderr)
62
+ sys.exit(1)
63
+ except Exception as e:
64
+ print(f"{RED}Unexpected error: {e}{NC}", file=sys.stderr)
65
+ sys.exit(1)
66
+
67
+ # Display results
68
+ for file_info in results['file_details']:
69
+ filepath = file_info['filepath']
70
+ issues = file_info['issues']
71
+
72
+ print(f"{YELLOW}File: {filepath}{NC}")
73
+
74
+ for issue in issues:
75
+ print(f" {RED}Line {issue['line_num']}:{NC} Code block without [source] directive")
76
+ print(f" Previous line ({issue['prev_line_num']}): {issue['prev_line']}")
77
+ print()
78
+
79
+ if args.fix:
80
+ if file_info.get('fixed'):
81
+ print(f" {GREEN}✓ Fixed {len(issues)} issue(s){NC}")
82
+ elif 'error' in file_info:
83
+ print(f" {RED}✗ Failed to fix file: {file_info['error']}{NC}")
84
+ print()
85
+
86
+ # Summary
87
+ print("=" * 64)
88
+ if results['total_issues'] == 0:
89
+ print(f"{GREEN}✓ No issues found!{NC}")
90
+ sys.exit(0)
91
+ else:
92
+ if args.fix:
93
+ print(f"{GREEN}Fixed {results['total_issues']} code block(s) in {results['files_fixed']} file(s){NC}")
94
+ sys.exit(0)
95
+ else:
96
+ print(f"{RED}Found {results['total_issues']} code block(s) missing [source] directive in {results['files_with_issues']} file(s){NC}")
97
+ print(f"\nRun with --fix to automatically fix these issues")
98
+ sys.exit(1)
99
+
100
+ if __name__ == '__main__':
101
+ main()
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ convert-tables-to-deflists: Convert AsciiDoc tables to definition lists.
4
+
5
+ Converts 2-column AsciiDoc tables to definition list format, where:
6
+ - The first column becomes the term
7
+ - The second column becomes the definition
8
+
9
+ Tables with more than 2 columns are skipped (use --columns to specify which
10
+ columns to use as term and definition).
11
+
12
+ Usage:
13
+ convert-tables-to-deflists [OPTIONS] [PATH]
14
+
15
+ Examples:
16
+ # Preview changes (dry-run mode)
17
+ convert-tables-to-deflists .
18
+
19
+ # Apply changes to all .adoc files
20
+ convert-tables-to-deflists --apply .
21
+
22
+ # Process a single file
23
+ convert-tables-to-deflists --apply path/to/file.adoc
24
+
25
+ # Use columns 1 and 3 for 3-column tables
26
+ convert-tables-to-deflists --columns 1,3 .
27
+
28
+ # Skip tables with headers
29
+ convert-tables-to-deflists --skip-header-tables .
30
+ """
31
+
32
+ import argparse
33
+ import sys
34
+ import re
35
+ from pathlib import Path
36
+ from typing import List, Optional, Tuple
37
+
38
+ from callout_lib.table_parser import TableParser, AsciiDocTable
39
+ from doc_utils.version import __version__
40
+ from doc_utils.file_utils import parse_exclude_list_file
41
+
42
+
43
+ class Colors:
44
+ """ANSI color codes for terminal output."""
45
+ RED = '\033[0;31m'
46
+ GREEN = '\033[0;32m'
47
+ YELLOW = '\033[1;33m'
48
+ BLUE = '\033[0;34m'
49
+ CYAN = '\033[0;36m'
50
+ NC = '\033[0m' # No Color
51
+
52
+
53
+ def print_colored(message: str, color: str = Colors.NC) -> None:
54
+ """Print a message with optional color."""
55
+ print(f"{color}{message}{Colors.NC}")
56
+
57
+
58
+ class TableToDeflistConverter:
59
+ """Converts AsciiDoc tables to definition lists."""
60
+
61
+ def __init__(self, dry_run: bool = True, verbose: bool = False,
62
+ columns: Optional[Tuple[int, int]] = None,
63
+ skip_header_tables: bool = False,
64
+ skip_callout_tables: bool = True):
65
+ """
66
+ Initialize the converter.
67
+
68
+ Args:
69
+ dry_run: If True, don't modify files (preview mode)
70
+ verbose: If True, show detailed output
71
+ columns: Tuple of (term_col, def_col) for multi-column tables (1-indexed)
72
+ skip_header_tables: If True, skip tables that have header rows
73
+ skip_callout_tables: If True, skip tables that look like callout tables
74
+ """
75
+ self.dry_run = dry_run
76
+ self.verbose = verbose
77
+ self.columns = columns # 1-indexed column numbers
78
+ self.skip_header_tables = skip_header_tables
79
+ self.skip_callout_tables = skip_callout_tables
80
+ self.parser = TableParser()
81
+ self.files_processed = 0
82
+ self.files_modified = 0
83
+ self.tables_converted = 0
84
+
85
+ def find_adoc_files(self, path: Path, exclude_dirs: List[str] = None,
86
+ exclude_files: List[str] = None) -> List[Path]:
87
+ """Find all .adoc files in the given path."""
88
+ exclude_dirs = exclude_dirs or []
89
+ exclude_files = exclude_files or []
90
+
91
+ if path.is_file():
92
+ return [path] if path.suffix == '.adoc' else []
93
+
94
+ adoc_files = []
95
+ for adoc_file in path.rglob('*.adoc'):
96
+ # Skip excluded directories
97
+ if any(excl in str(adoc_file) for excl in exclude_dirs):
98
+ continue
99
+ # Skip excluded files
100
+ if any(excl in str(adoc_file) for excl in exclude_files):
101
+ continue
102
+ # Skip symlinks
103
+ if adoc_file.is_symlink():
104
+ continue
105
+ adoc_files.append(adoc_file)
106
+
107
+ return sorted(adoc_files)
108
+
109
+ def _should_skip_table(self, table: AsciiDocTable) -> Tuple[bool, str]:
110
+ """
111
+ Determine if a table should be skipped.
112
+
113
+ Returns:
114
+ Tuple of (should_skip, reason)
115
+ """
116
+ # Skip empty tables
117
+ if not table.rows:
118
+ return True, "empty table"
119
+
120
+ # Skip callout tables (they're handled by convert-callouts-to-deflist)
121
+ if self.skip_callout_tables:
122
+ if self.parser.is_callout_table(table) or self.parser.is_3column_callout_table(table):
123
+ return True, "callout table (use convert-callouts-to-deflist)"
124
+
125
+ # Check column count
126
+ if table.rows:
127
+ first_row_cols = len(table.rows[0].cells)
128
+
129
+ # If specific columns are specified, verify they exist
130
+ if self.columns:
131
+ term_col, def_col = self.columns
132
+ if term_col > first_row_cols or def_col > first_row_cols:
133
+ return True, f"specified columns ({term_col}, {def_col}) exceed table columns ({first_row_cols})"
134
+ else:
135
+ # Default: only process 2-column tables
136
+ if first_row_cols != 2:
137
+ return True, f"{first_row_cols}-column table (use --columns to specify term and definition columns)"
138
+
139
+ # Check for header row
140
+ if self.skip_header_tables and self.parser._has_header_row(table):
141
+ return True, "table has header row"
142
+
143
+ return False, ""
144
+
145
+ def _convert_table_to_deflist(self, table: AsciiDocTable) -> List[str]:
146
+ """
147
+ Convert a table to definition list format.
148
+
149
+ Args:
150
+ table: The AsciiDocTable to convert
151
+
152
+ Returns:
153
+ List of lines representing the definition list
154
+ """
155
+ output = []
156
+
157
+ # Determine which columns to use (0-indexed internally)
158
+ if self.columns:
159
+ term_idx = self.columns[0] - 1 # Convert to 0-indexed
160
+ def_idx = self.columns[1] - 1
161
+ else:
162
+ term_idx = 0
163
+ def_idx = 1
164
+
165
+ # Check if table has a header row
166
+ has_header = self.parser._has_header_row(table)
167
+ data_rows = table.rows[1:] if has_header else table.rows
168
+
169
+ for row in data_rows:
170
+ # Verify row has enough cells
171
+ if len(row.cells) <= max(term_idx, def_idx):
172
+ continue
173
+
174
+ # Add conditionals before row
175
+ if row.conditionals_before:
176
+ output.extend(row.conditionals_before)
177
+
178
+ # Get term (first specified column)
179
+ term_cell = row.cells[term_idx]
180
+ term = ' '.join(line.strip() for line in term_cell.content if line.strip())
181
+
182
+ # Get definition (second specified column)
183
+ def_cell = row.cells[def_idx]
184
+ def_lines = def_cell.content
185
+
186
+ # Create definition list entry
187
+ if term:
188
+ output.append(f'{term}::')
189
+
190
+ # Add definition lines
191
+ first_content_line = True
192
+ for line in def_lines:
193
+ stripped = line.strip()
194
+
195
+ # Handle conditional directives
196
+ if stripped.startswith(('ifdef::', 'ifndef::', 'endif::')):
197
+ output.append(line)
198
+ continue
199
+
200
+ # Skip empty lines within definition but track them
201
+ if not stripped:
202
+ continue
203
+
204
+ # First content line gets no indent, subsequent lines do
205
+ if first_content_line:
206
+ output.append(stripped)
207
+ first_content_line = False
208
+ else:
209
+ output.append(f'+\n{stripped}')
210
+
211
+ # Add blank line after entry
212
+ output.append('')
213
+
214
+ # Add conditionals after row
215
+ if row.conditionals_after:
216
+ output.extend(row.conditionals_after)
217
+
218
+ # Remove trailing blank line if present
219
+ if output and not output[-1].strip():
220
+ output.pop()
221
+
222
+ return output
223
+
224
+ def process_file(self, file_path: Path) -> int:
225
+ """
226
+ Process a single file, converting tables to definition lists.
227
+
228
+ Args:
229
+ file_path: Path to the .adoc file
230
+
231
+ Returns:
232
+ Number of tables converted
233
+ """
234
+ try:
235
+ with open(file_path, 'r', encoding='utf-8') as f:
236
+ lines = [line.rstrip('\n') for line in f]
237
+ except Exception as e:
238
+ print_colored(f"Error reading {file_path}: {e}", Colors.RED)
239
+ return 0
240
+
241
+ original_lines = lines.copy()
242
+ tables = self.parser.find_tables(lines)
243
+ conversions = 0
244
+
245
+ # Process tables in reverse order to preserve line numbers
246
+ for table in reversed(tables):
247
+ should_skip, reason = self._should_skip_table(table)
248
+
249
+ if should_skip:
250
+ if self.verbose:
251
+ print(f" Skipping table at line {table.start_line + 1}: {reason}")
252
+ continue
253
+
254
+ # Convert the table
255
+ deflist_lines = self._convert_table_to_deflist(table)
256
+
257
+ if deflist_lines:
258
+ # Replace table with definition list
259
+ lines[table.start_line:table.end_line + 1] = deflist_lines
260
+ conversions += 1
261
+
262
+ if self.verbose:
263
+ print(f" Converted table at line {table.start_line + 1}")
264
+
265
+ # Write changes if not in dry-run mode
266
+ if conversions > 0:
267
+ if self.dry_run:
268
+ print_colored(f"Would modify: {file_path} ({conversions} table(s))", Colors.YELLOW)
269
+ else:
270
+ try:
271
+ with open(file_path, 'w', encoding='utf-8') as f:
272
+ f.write('\n'.join(lines) + '\n')
273
+ print_colored(f"Modified: {file_path} ({conversions} table(s))", Colors.GREEN)
274
+ except Exception as e:
275
+ print_colored(f"Error writing {file_path}: {e}", Colors.RED)
276
+ return 0
277
+
278
+ return conversions
279
+
280
+ def process_path(self, path: Path, exclude_dirs: List[str] = None,
281
+ exclude_files: List[str] = None) -> None:
282
+ """
283
+ Process all .adoc files in the given path.
284
+
285
+ Args:
286
+ path: File or directory path to process
287
+ exclude_dirs: List of directory patterns to exclude
288
+ exclude_files: List of file patterns to exclude
289
+ """
290
+ adoc_files = self.find_adoc_files(path, exclude_dirs, exclude_files)
291
+
292
+ if not adoc_files:
293
+ print_colored("No .adoc files found.", Colors.YELLOW)
294
+ return
295
+
296
+ if self.dry_run:
297
+ print_colored("DRY RUN MODE - No files will be modified", Colors.YELLOW)
298
+ print()
299
+
300
+ for file_path in adoc_files:
301
+ self.files_processed += 1
302
+ conversions = self.process_file(file_path)
303
+
304
+ if conversions > 0:
305
+ self.files_modified += 1
306
+ self.tables_converted += conversions
307
+
308
+ # Print summary
309
+ print()
310
+ print(f"Processed {self.files_processed} file(s)")
311
+ print(f"Tables converted: {self.tables_converted}")
312
+ print(f"Files {'would be ' if self.dry_run else ''}modified: {self.files_modified}")
313
+
314
+ if self.dry_run and self.files_modified > 0:
315
+ print()
316
+ print_colored("DRY RUN - No files were modified. Use --apply to apply changes.", Colors.YELLOW)
317
+
318
+
319
+ def parse_columns(columns_str: str) -> Tuple[int, int]:
320
+ """
321
+ Parse a columns specification like "1,3" into a tuple.
322
+
323
+ Args:
324
+ columns_str: String like "1,3" specifying term and definition columns
325
+
326
+ Returns:
327
+ Tuple of (term_column, definition_column) as 1-indexed integers
328
+
329
+ Raises:
330
+ argparse.ArgumentTypeError: If the format is invalid
331
+ """
332
+ try:
333
+ parts = columns_str.split(',')
334
+ if len(parts) != 2:
335
+ raise ValueError("Expected exactly two column numbers")
336
+ term_col = int(parts[0].strip())
337
+ def_col = int(parts[1].strip())
338
+ if term_col < 1 or def_col < 1:
339
+ raise ValueError("Column numbers must be 1 or greater")
340
+ if term_col == def_col:
341
+ raise ValueError("Term and definition columns must be different")
342
+ return (term_col, def_col)
343
+ except ValueError as e:
344
+ raise argparse.ArgumentTypeError(
345
+ f"Invalid columns format '{columns_str}': {e}. "
346
+ "Use format like '1,2' or '1,3' (1-indexed column numbers)"
347
+ )
348
+
349
+
350
+ def main() -> int:
351
+ """Main entry point for the CLI."""
352
+ parser = argparse.ArgumentParser(
353
+ description='Convert AsciiDoc tables to definition lists.',
354
+ formatter_class=argparse.RawDescriptionHelpFormatter,
355
+ epilog="""
356
+ Examples:
357
+ # Preview changes (default dry-run mode)
358
+ convert-tables-to-deflists .
359
+
360
+ # Apply changes to all .adoc files
361
+ convert-tables-to-deflists --apply .
362
+
363
+ # Process a single file
364
+ convert-tables-to-deflists --apply path/to/file.adoc
365
+
366
+ # For 3-column tables, use columns 1 and 3
367
+ convert-tables-to-deflists --columns 1,3 .
368
+
369
+ # Skip tables that have header rows
370
+ convert-tables-to-deflists --skip-header-tables .
371
+
372
+ Notes:
373
+ - By default, only 2-column tables are converted
374
+ - Callout tables are automatically skipped (use convert-callouts-to-deflist)
375
+ - Use --columns to specify which columns to use for multi-column tables
376
+ - The first specified column becomes the term, the second becomes the definition
377
+ """
378
+ )
379
+
380
+ parser.add_argument(
381
+ '--version',
382
+ action='version',
383
+ version=f'%(prog)s {__version__}'
384
+ )
385
+
386
+ parser.add_argument(
387
+ 'path',
388
+ nargs='?',
389
+ default='.',
390
+ help='File or directory to process (default: current directory)'
391
+ )
392
+
393
+ parser.add_argument(
394
+ '--apply',
395
+ action='store_true',
396
+ help='Apply changes (default is dry-run mode)'
397
+ )
398
+
399
+ parser.add_argument(
400
+ '-v', '--verbose',
401
+ action='store_true',
402
+ help='Show detailed output'
403
+ )
404
+
405
+ parser.add_argument(
406
+ '--columns',
407
+ type=parse_columns,
408
+ metavar='TERM,DEF',
409
+ help='Column numbers to use as term and definition (1-indexed, e.g., "1,3")'
410
+ )
411
+
412
+ parser.add_argument(
413
+ '--skip-header-tables',
414
+ action='store_true',
415
+ help='Skip tables that have header rows'
416
+ )
417
+
418
+ parser.add_argument(
419
+ '--include-callout-tables',
420
+ action='store_true',
421
+ help='Include callout tables (normally skipped)'
422
+ )
423
+
424
+ parser.add_argument(
425
+ '--exclude-dir',
426
+ action='append',
427
+ default=[],
428
+ metavar='DIR',
429
+ help='Directory pattern to exclude (can be specified multiple times)'
430
+ )
431
+
432
+ parser.add_argument(
433
+ '--exclude-file',
434
+ action='append',
435
+ default=[],
436
+ metavar='FILE',
437
+ help='File pattern to exclude (can be specified multiple times)'
438
+ )
439
+
440
+ parser.add_argument(
441
+ '--exclude-list',
442
+ type=Path,
443
+ metavar='FILE',
444
+ help='Path to file containing exclusion patterns (one per line)'
445
+ )
446
+
447
+ args = parser.parse_args()
448
+
449
+ # Parse exclusion list if provided
450
+ exclude_dirs = list(args.exclude_dir)
451
+ exclude_files = list(args.exclude_file)
452
+
453
+ if args.exclude_list:
454
+ list_dirs, list_files = parse_exclude_list_file(args.exclude_list)
455
+ exclude_dirs.extend(list_dirs)
456
+ exclude_files.extend(list_files)
457
+
458
+ # Create converter
459
+ converter = TableToDeflistConverter(
460
+ dry_run=not args.apply,
461
+ verbose=args.verbose,
462
+ columns=args.columns,
463
+ skip_header_tables=args.skip_header_tables,
464
+ skip_callout_tables=not args.include_callout_tables
465
+ )
466
+
467
+ # Process files
468
+ path = Path(args.path)
469
+ if not path.exists():
470
+ print_colored(f"Error: Path does not exist: {path}", Colors.RED)
471
+ return 1
472
+
473
+ converter.process_path(path, exclude_dirs, exclude_files)
474
+
475
+ return 0
476
+
477
+
478
+ if __name__ == '__main__':
479
+ sys.exit(main())