src/litterate/generator.py annotated source
Back to indexGenerator Module
This module contains the bulk of the logic for generating litterate pages. It exports a function that the command-line utility calls with configurations.
56import html7from pathlib import Path8from typing import Any910import markdown11from jinja2 import Template12This isn't optimal, but for now, we read the three template files into memory at module load time, so we can reuse them later without repeatedly reading from disk.
1516TEMPLATE_DIR = Path(__file__).parent / "templates"17INDEX_TEMPLATE = Template((TEMPLATE_DIR / "index.html").read_text())18STYLES_CSS = (TEMPLATE_DIR / "main.css").read_text()19SOURCE_TEMPLATE = Template((TEMPLATE_DIR / "source.html").read_text())2021Language Detection
Detect the programming language from file extension for syntax highlighting. If the user specifies a language in the config, we use that instead of auto-detection.
2627def detect_language(file_path: Path, config: dict[str, Any]) -> str:28 if "language" in config and config["language"] != "auto":29 return config["language"]30Map common file extensions to Highlight.js language identifiers. This covers most popular programming languages.
33 ext_map = {34 ".py": "python",35 ".js": "javascript",36 ".jsx": "javascript",37 ".ts": "typescript",38 ".tsx": "typescript",39 ".java": "java",40 ".cpp": "cpp",41 ".cc": "cpp",42 ".cxx": "cpp",43 ".c": "c",44 ".h": "c",45 ".hpp": "cpp",46 ".rs": "rust",47 ".go": "go",48 ".rb": "ruby",49 ".php": "php",50 ".sh": "bash",51 ".bash": "bash",52 ".zsh": "bash",53 ".fish": "bash",54 ".swift": "swift",55 ".kt": "kotlin",56 ".scala": "scala",57 ".r": "r",58 ".sql": "sql",59 ".html": "html",60 ".xml": "xml",61 ".css": "css",62 ".scss": "scss",63 ".sass": "sass",64 ".json": "json",65 ".yaml": "yaml",66 ".yml": "yaml",67 ".toml": "toml",68 ".md": "markdown",69 ".rst": "rst",70 ".tex": "latex",71 ".vim": "vim",72 ".lua": "lua",73 ".pl": "perl",74 ".pm": "perl",75 }7677 suffix = file_path.suffix.lower()78 return ext_map.get(suffix, "plaintext")7980Helper Functions
These utility functions help with text processing and path management.
84Line Wrapping: Helper function to wrap a given line of text into multiple lines,
with limit characters per line.
8788def wrap_line(line: str, limit: int) -> str:89 result = []90 for i in range(0, len(line), limit):91 result.append(line[i:i + limit])92 return "\n".join(result)9394HTML Encoding: Escape characters that won't display in HTML correctly,
like the very common >, <, and & characters in code.
9798def encode_html(code: str) -> str:99 return html.escape(code)100101Path Mapping: Function that maps a given source file to the path where its annotated version will be saved.
104105def get_output_path_for_source_path(source_path: Path, config: dict[str, Any]) -> Path:106 output_dir = Path(config["output_directory"])107 return output_dir / f"{source_path}.html"108109Index Page Generation
Function to populate the index.html page of the generated site with all
the source links, project name, description, etc.
114115def populate_index_page(source_files: list[Path], config: dict[str, Any]) -> str:116 output_dir = Path(config["output_directory"])117 base_url = config["baseURL"]118Generate links for each source file, handling both local and web deployment scenarios.
120 files_html = []121 for source_path in source_files:122 output_path = get_output_path_for_source_path(source_path, config)123 relative_path = output_path.relative_to(output_dir)124125 if base_url == "./":126 link = str(relative_path)127 else:128 link = f"{base_url}{relative_path}"129130 files_html.append(131 f'<p class="sourceLink"><a href="{link}">{source_path}</a></p>'132 )133For CSS link, handle baseURL properly. The template appends "main.css", so just pass the directory path.
136 css_base = "" if base_url == "./" else base_url137138 return INDEX_TEMPLATE.render(139 title=config["name"],140 description=markdown.markdown(config["description"]),141 sourcesList="\n".join(files_html),142 baseURL=css_base,143 )144145Parsing Source Lines
lines_to_line_pairs works by having two arrays -- one of the annotation-lineNumber-source line
tuples, and one buffer for the annotation text being read.
150151def lines_to_line_pairs(152 lines: list[str], config: dict[str, Any]153) -> list[tuple[str, str, int]]:154 line_pairs = []155 doc_line = ""156 in_annotation_comment = False157158 annotation_start = config["annotation_start_mark"]159 annotation_continue = config["annotation_continue_mark"]160 wrap_limit = config["wrap"]161162 def process_code_line(code_line: str) -> str:163 encoded = encode_html(code_line)164 if wrap_limit != 0:165 return wrap_line(encoded, wrap_limit)166 return encoded167168 def push_pair(code_line: str, line_number: int) -> None:169 nonlocal doc_line170171 if doc_line:Add spacing between annotation blocks for better readability
173 if line_pairs and line_pairs[-1][0]:174 line_pairs.append(("", "", ""))175 line_pairs.append(176 (markdown.markdown(doc_line), process_code_line(code_line), line_number)177 )178 else:179 line_pairs.append(("", process_code_line(code_line), line_number))180181 doc_line = ""182183 def push_comment(line: str) -> None:184 nonlocal doc_line185186 if line.strip().startswith(annotation_start):187 doc_line = line.replace(annotation_start, "", 1).strip()188 else:189 doc_line += "\n" + line.replace(annotation_continue, "", 1).strip()190Main parsing loop: iterate through source lines and identify annotation comments vs regular code lines.
193 for idx, line in enumerate(lines):194 stripped = line.strip()195196 if stripped.startswith(annotation_start):197 in_annotation_comment = True198 push_comment(line)199 elif stripped.startswith(annotation_continue):200 if in_annotation_comment:201 push_comment(line)202 else:203 push_pair(line, idx + 1)204 else:205 if in_annotation_comment:206 in_annotation_comment = False207 push_pair(line, idx + 1)208209 return line_pairs210211Page Creation
This function is called for each source file, to process and save the Litterate version of the source file in the correct place.
216217def create_and_save_page(source_path: Path, config: dict[str, Any]) -> None:218 content = source_path.read_text(encoding="utf-8")219220 line_pairs = lines_to_line_pairs(content.split("\n"), config)221222 language = detect_language(source_path, config)223Generate HTML for each line, combining documentation and source code
225 lines_html = []226 for doc, source, line_number in line_pairs:227 lines_html.append(228 f'<div class="line">'229 f'<div class="doc">{doc}</div>'230 f'<pre class="source language-{language}">'231 f'<strong class="lineNumber">{line_number}</strong>'232 f'<code class="language-{language}">{source}</code>'233 f'</pre>'234 f'</div>'235 )236Calculate relative paths for navigation, handling both local file viewing and web deployment
238 output_dir = Path(config["output_directory"])239 output_path = get_output_path_for_source_path(source_path, config)240 base_url = config["baseURL"]241242 if base_url == "./":243 depth = len(output_path.relative_to(output_dir).parts) - 1244 base_link = "../" * depth if depth > 0 else "./"245 css_link = base_link + "main.css"246 else:247 base_link = base_url248 css_link = f"{base_url}main.css"249250 annotated_page = SOURCE_TEMPLATE.render(251 title=str(source_path),252 lines="\n".join(lines_html),253 baseURL=base_link,254 cssURL=css_link,255 )256Save to output directory, creating parent directories as needed
258 output_path.parent.mkdir(parents=True, exist_ok=True)259 output_path.write_text(annotated_page, encoding="utf-8")260261Main Entry Point
This is the main function called by the CLI to generate literate documentation for all specified source files.
266267def generate_litterate_pages(source_files: list[Path], config: dict[str, Any]) -> None:268 output_directory = Path(config["output_directory"])269270 output_directory.mkdir(parents=True, exist_ok=True)271Generate and write the index page listing all source files
273 index_html = populate_index_page(source_files, config)274 (output_directory / "index.html").write_text(index_html, encoding="utf-8")275Write the CSS stylesheet
277 (output_directory / "main.css").write_text(STYLES_CSS, encoding="utf-8")278Process each source file, generating its annotated page
280 for source_file in source_files:281 create_and_save_page(source_file, config)282 print(f"Annotated {source_file}")283