| 1 | """Find SVG and notes files in a project directory.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import re |
| 6 | from pathlib import Path |
| 7 | |
| 8 | from slide_roster import discover_slide_svgs |
| 9 | |
| 10 | |
| 11 | class NotesFileReadError(RuntimeError): |
| 12 | """Report a matched notes file that cannot be decoded or read.""" |
| 13 | |
| 14 | |
| 15 | def find_svg_files( |
| 16 | project_path: Path, |
| 17 | source: str = 'output', |
| 18 | *, |
| 19 | allow_fallback: bool = True, |
| 20 | ) -> tuple[list[Path], str]: |
| 21 | """Find SVG files in the project. |
| 22 | |
| 23 | Args: |
| 24 | project_path: Project directory path. |
| 25 | source: SVG source directory alias or name. |
| 26 | - 'output': svg_output (hand-authored source; native default) |
| 27 | - 'final': svg_final (post-processed preview; diagnostic input) |
| 28 | - or any subdirectory name |
| 29 | allow_fallback: Try svg_output and then the project root when the |
| 30 | requested directory is missing. |
| 31 | |
| 32 | Returns: |
| 33 | (list_of_svg_files, actual_directory_name) tuple. |
| 34 | """ |
| 35 | dir_map = { |
| 36 | 'output': 'svg_output', |
| 37 | 'final': 'svg_final', |
| 38 | } |
| 39 | |
| 40 | dir_name = dir_map.get(source, source) |
| 41 | svg_dir = project_path / dir_name |
| 42 | |
| 43 | if not svg_dir.exists(): |
| 44 | if not allow_fallback: |
| 45 | return [], dir_name |
| 46 | print(f" Warning: {dir_name} directory does not exist, trying svg_output") |
| 47 | dir_name = 'svg_output' |
| 48 | svg_dir = project_path / dir_name |
| 49 | |
| 50 | if not svg_dir.exists(): |
| 51 | if project_path.is_dir(): |
| 52 | svg_dir = project_path |
| 53 | dir_name = project_path.name |
| 54 | else: |
| 55 | return [], '' |
| 56 | |
| 57 | return discover_slide_svgs(svg_dir), dir_name |
| 58 | |
| 59 | |
| 60 | def find_notes_files( |
| 61 | project_path: Path, |
| 62 | svg_files: list[Path] | None = None, |
| 63 | ) -> dict[str, str]: |
| 64 | """Find notes files and map them to SVG files. |
| 65 | |
| 66 | Supports two matching modes (mixed matching supported): |
| 67 | 1. Match by filename (priority): notes/01_cover.md -> 01_cover.svg |
| 68 | 2. Match by index (backward compatible): notes/slide01.md -> 1st SVG |
| 69 | |
| 70 | Args: |
| 71 | project_path: Project directory path. |
| 72 | svg_files: SVG file list (for filename matching). |
| 73 | |
| 74 | Returns: |
| 75 | Dict mapping SVG filename stem to notes content. |
| 76 | """ |
| 77 | notes_dir = project_path / 'notes' |
| 78 | notes: dict[str, str] = {} |
| 79 | |
| 80 | if not notes_dir.exists(): |
| 81 | return notes |
| 82 | |
| 83 | svg_stems_mapping: dict[str, int] = {} |
| 84 | svg_index_mapping: dict[int, str] = {} |
| 85 | if svg_files: |
| 86 | for i, svg_path in enumerate(svg_files, 1): |
| 87 | svg_stems_mapping[svg_path.stem] = i |
| 88 | svg_index_mapping[i] = svg_path.stem |
| 89 | |
| 90 | for notes_file in notes_dir.glob('*.md'): |
| 91 | stem = notes_file.stem |
| 92 | |
| 93 | # Try index-based matching (backward compat with slide01.md format). |
| 94 | match = re.search(r'slide[_]?(\d+)', stem) |
| 95 | mapped_stem = ( |
| 96 | svg_index_mapping.get(int(match.group(1))) |
| 97 | if match |
| 98 | else None |
| 99 | ) |
| 100 | filename_match = stem in svg_stems_mapping |
| 101 | if mapped_stem is None and not filename_match: |
| 102 | continue |
| 103 | |
| 104 | try: |
| 105 | content = notes_file.read_text(encoding='utf-8').strip() |
| 106 | except (OSError, UnicodeError) as exc: |
| 107 | raise NotesFileReadError( |
| 108 | f"Cannot read matched notes file {notes_file}: {exc}" |
| 109 | ) from exc |
| 110 | if not content: |
| 111 | continue |
| 112 | |
| 113 | if mapped_stem: |
| 114 | notes[mapped_stem] = content |
| 115 | |
| 116 | # Filename-based matching overrides index-based matching. |
| 117 | if filename_match: |
| 118 | notes[stem] = content |
| 119 | |
| 120 | return notes |
| 121 |