Coverage for src/pullapprove/docs.py: 0%
109 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-08-04 15:31 -0500
« prev ^ index » next coverage.py v7.8.2, created at 2025-08-04 15:31 -0500
1from collections.abc import Iterator
2from pathlib import Path
4import click
7def get_docs_dir(version: str | None = None) -> Path:
8 """Get the path to the documentation directory."""
9 base_dir = Path(__file__).parent / "docs"
10 if version:
11 return base_dir / version
12 return base_dir
15def get_all_docs(version: str | None = None) -> list[Path]:
16 """Get all markdown documentation files sorted by path."""
17 docs_dir = get_docs_dir(version)
18 return sorted(docs_dir.rglob("*.md"))
21def remove_frontmatter(content: str) -> str:
22 """
23 Remove YAML frontmatter from markdown content.
24 """
25 lines = content.splitlines()
26 if lines and lines[0].strip() == "---":
27 # Find the closing ---
28 for i in range(1, len(lines)):
29 if lines[i].strip() == "---":
30 # Return content after frontmatter
31 return "\n".join(lines[i + 1 :])
32 return content
35def iterate_markdown(content: str) -> Iterator[str]:
36 """
37 Iterator that does basic markdown formatting for a Click pager.
39 Headings are yellow and bright, code blocks are indented.
40 """
41 in_code_block = False
42 for line in content.splitlines():
43 if line.startswith("```"):
44 in_code_block = not in_code_block
46 if in_code_block:
47 yield click.style(line, dim=True)
48 elif line.startswith("# "):
49 yield click.style(line, fg="yellow", bold=True)
50 elif line.startswith("## "):
51 yield click.style(line, fg="yellow", bold=True)
52 elif line.startswith("### "):
53 yield click.style(line, fg="yellow", bold=True)
54 elif line.startswith("#### "):
55 yield click.style(line, fg="yellow", bold=True)
56 elif line.startswith("##### "):
57 yield click.style(line, fg="yellow", bold=True)
58 elif line.startswith("###### "):
59 yield click.style(line, fg="yellow", bold=True)
60 elif line.startswith("**") and line.endswith("**"):
61 yield click.style(line, bold=True)
62 elif line.startswith("> "):
63 yield click.style(line, italic=True)
64 else:
65 yield line
67 yield "\n"
70def list_docs_tree(version: str | None = None) -> None:
71 """Display available documentation in a tree format."""
72 docs_dir = get_docs_dir(version)
73 docs_files = get_all_docs(version)
75 # Group files by directory for tree display
76 dirs: dict[str, list[str]] = {}
77 for doc in docs_files:
78 rel_path = doc.relative_to(docs_dir)
79 dir_name = str(rel_path.parent) if rel_path.parent != Path(".") else ""
80 if dir_name not in dirs:
81 dirs[dir_name] = []
82 dirs[dir_name].append(rel_path.stem)
84 # Display as tree
85 for dir_name in sorted(dirs.keys()):
86 if dir_name:
87 click.echo(f"{dir_name}/")
88 for file in sorted(dirs[dir_name]):
89 click.echo(f" {file}")
90 else:
91 for file in sorted(dirs[dir_name]):
92 click.echo(file)
95def show_all_docs(version: str | None = None) -> None:
96 """Display all documentation concatenated with separators."""
97 docs_dir = get_docs_dir(version)
98 docs_files = get_all_docs(version)
100 # Concatenate all docs with headers
101 all_content = []
102 for doc_file in docs_files:
103 rel_path = doc_file.relative_to(docs_dir)
104 content = remove_frontmatter(doc_file.read_text())
105 # Add a separator and file path header
106 all_content.append(f"\n{'=' * 60}\n{rel_path}\n{'=' * 60}\n")
107 all_content.append(content)
109 click.echo_via_pager(iterate_markdown("\n".join(all_content)))
112def show_doc_by_name(name: str, version: str | None = None) -> bool:
113 """
114 Display a specific documentation file by name.
115 Returns True if found, False otherwise.
116 """
117 docs_dir = get_docs_dir(version)
118 docs_files = get_all_docs(version)
120 # Try exact match first
121 for f in docs_files:
122 if f.stem == name:
123 content = remove_frontmatter(f.read_text())
124 click.echo_via_pager(iterate_markdown(content))
125 return True
127 # Try matching with directory prefix (e.g., "scopes/paths")
128 for f in docs_files:
129 rel_path = f.relative_to(docs_dir).with_suffix("")
130 if str(rel_path) == name:
131 content = remove_frontmatter(f.read_text())
132 click.echo_via_pager(iterate_markdown(content))
133 return True
135 return False
138def show_doc_not_found(name: str, version: str | None = None) -> None:
139 """Display error message when a doc is not found."""
140 version_str = f" (v{version})" if version else ""
141 click.secho(f"Documentation for '{name}' not found{version_str}.", fg="red")
142 click.echo("\nAvailable docs:")
143 list_docs_tree(version)
146def show_default_doc(version: str | None = None) -> None:
147 """Display the default documentation (index.md)."""
148 docs_dir = get_docs_dir(version)
149 index_file = docs_dir / "index.md"
151 if index_file.exists():
152 content = remove_frontmatter(index_file.read_text())
153 click.echo_via_pager(iterate_markdown(content))
154 else:
155 version_str = f" for v{version}" if version else ""
156 click.echo(
157 f"Use --list to see available documentation{version_str} or specify a doc name."
158 )
161def get_doc_content(name: str, version: str | None = None) -> str | None:
162 """
163 Get the content of a specific documentation file by name.
164 Returns the content with frontmatter removed, or None if not found.
165 """
166 docs_dir = get_docs_dir(version)
167 docs_files = get_all_docs(version)
169 # Try exact match first
170 for f in docs_files:
171 if f.stem == name:
172 return remove_frontmatter(f.read_text())
174 # Try matching with directory prefix (e.g., "scopes/paths")
175 for f in docs_files:
176 rel_path = f.relative_to(docs_dir).with_suffix("")
177 if str(rel_path) == name:
178 return remove_frontmatter(f.read_text())
180 return None