Source code for gitlab_overviewer.rendering.markdown

  1"""
  2Markdown renderer implementation.
  3
  4Implements :any:`/specs/spec_renderer_markdown` §1-4, covering:
  5
  6* Top-level layout (§1)
  7* Summary tables (§2)
  8* Project detail sections (§3)
  9* Ordering guarantees (§4)
 10"""
 11
 12from typing import List
 13
 14from markdown_it import MarkdownIt
 15from mdit_py_plugins.front_matter import front_matter_plugin
 16
 17from ..models.overview_data import OverviewData
 18from ..services.readme_extraction import parse_readme
 19from ..rendering.table import render_table
 20from ..services.sort_utils import sort_overview
 21from .renderer_base import Renderer
 22
 23
[docs] 24class MarkdownRenderer(Renderer): 25 """ 26 Markdown renderer implementation as a class. 27 Implements /specs/spec_renderer_markdown.md §6 interface. 28 """ 29
[docs] 30 def __init__(self, table_config: dict): 31 self.table_config = table_config
32
[docs] 33 def write_files( 34 self, 35 overview_data: List[OverviewData], 36 output_file: str = "Overview.md", 37 *args, 38 **kwargs, 39 ) -> None: 40 """ 41 Write the rendered markdown output to Overview.md. 42 See /specs/spec_renderer_markdown.md §6 for file output requirements. 43 """ 44 output = self.render(overview_data) 45 with open(output_file, "w", encoding="utf-8") as f: 46 f.write(output)
47
[docs] 48 def render(self, overview_data: List[OverviewData], *args, **kwargs) -> str: 49 """ 50 Render the grouped markdown overview. 51 See /specs/spec_renderer_markdown.md §6 for required output. 52 """ 53 54 # Sort the overview data according to table_config 55 overview_data = sort_overview(overview_data, self.table_config) 56 57 out = "# Overview over GitLab Readmes\n\n" 58 # Group data by group name 59 group_map = {} 60 for od in overview_data: 61 group_map.setdefault(od.group.name, []).append(od) 62 63 # Sort groups by number of projects 64 sorted_groups = sorted( 65 group_map.items(), key=lambda item: (-len(item[1]), item[0].lower()) 66 ) 67 68 # --- 1. Summary Section --- 69 out += "## Summary\n\n" 70 for group_name, ods in sorted_groups: 71 out += f"### {group_name}\n\n" 72 73 # Prepare data for table rendering 74 table_data = [] 75 for od in ods: 76 p = od.project 77 row = od.model_dump() 78 79 # Use absolute URL for repo link 80 if p.web_url: 81 row["repo"] = f"[{p.name}]({p.web_url})" 82 if p.avatar_url: 83 row["repo"] = ( 84 f"![Avatar]({p.avatar_url}){{width=16px}} {row['repo']}" 85 ) 86 else: 87 row["repo"] = p.name 88 table_data.append((p.name, row)) 89 90 out += render_table(table_data, self.table_config) 91 out += "\n" 92 out += "Note: If no supervisors were found, authors of the README are named as supervisors.\n\n" 93 94 # --- 2. Detailed Section --- 95 for group_name, ods in sorted_groups: 96 out += f"## Group {group_name}\n\n" 97 for od in sorted(ods, key=lambda p: p.project.name.lower()): 98 out += self.render_project_section(od) 99 return out
100
[docs] 101 def render_project_section(self, od: OverviewData) -> str: 102 """Render a detailed section for a project""" 103 p = od.project 104 section = f"### {p.name}\n\n" 105 106 details = {} 107 if ( 108 "date" in od.extra.raw_frontmatter 109 and od.extra.raw_frontmatter["date"] != "-" 110 ): 111 details["Date"] = od.extra.raw_frontmatter["date"] 112 113 status = ( 114 od.extra.raw_frontmatter.get("status") 115 if "status" in od.extra.raw_frontmatter 116 else getattr(p, "status", None) 117 ) 118 if status and status != "-": 119 details["Status"] = status 120 121 supervisors = ( 122 "; ".join(od.extra.supervisors) 123 if od.extra.supervisors 124 else ( 125 "Authors: " + "; ".join(od.extra.authors) if od.extra.authors else None 126 ) 127 ) 128 if supervisors and supervisors != "-": 129 details["Supervisors"] = supervisors 130 131 if details: 132 for key, value in details.items(): 133 if isinstance(value, list): 134 section += f"{key}\n" 135 for item in value: 136 section += f": {item}\n" 137 section += "\n" 138 else: 139 section += f"{key}\n: {value}\n\n" 140 141 if p.web_url: 142 section += f"[![Release]({p.web_url}/-/badges/release.svg)]({p.web_url}/-/releases)\n\n" 143 144 # Add callouts (tip, note, etc.) 145 if ( 146 od.readme 147 and od.readme.extra.raw_frontmatter 148 and "description" in od.readme.extra.raw_frontmatter 149 ): 150 section += "> [!tip] Description\n>\n" 151 section += f"> {od.readme.extra.raw_frontmatter['description']}\n\n" 152 elif od.readme and od.readme.content and od.readme.content.strip(): 153 # Use parse_readme to strip YAML front-matter 154 parsed = parse_readme(od.readme.content) 155 # Ensure parsed is a string (not a dict) 156 if isinstance(parsed, dict): 157 cleaned_content = parsed.get("content", "") 158 else: 159 cleaned_content = parsed 160 lines = [ 161 "> " + line for line in cleaned_content.splitlines() if line.strip() 162 ] 163 truncated_content = "\n".join(lines[:10]) 164 if len(lines) > 10: 165 truncated_content += "\n> …continues…" 166 section += f"> [!warning] Description\n>\n> {truncated_content}\n\n" 167 elif not od.readme or not od.readme.content: 168 section += "> [!danger] Description\n>\n> No Readme found\n\n" 169 170 # Issues summary callout logic 171 if od.issues: 172 section += f"> [!tip] Open Issues \n> \n" 173 for issue in od.issues: 174 section += f"> - [{issue.title}]({issue.web_url})\n" 175 section += f"> \n> TOTAL: opened: {len(od.issues)}\n\n" 176 elif ( 177 od.readme 178 and getattr(od.readme, "todo", None) 179 and str(od.readme.todo).strip() 180 ): 181 todos = ["> " + line for line in str(od.readme.todo).strip().split("\n")] 182 todo_readme = "\n".join(todos[:10]) 183 if len(todos) > 10: 184 todo_readme += "> …continues…" 185 186 section += f"> [!warning] Open Issues\n> \n> {todo_readme}\n\n" 187 else: 188 section += "> [!danger] Open Issues\n> \n> No TODOs found.\n\n" 189 190 if p.readme_url: 191 section += f"[Link to full readme]({p.readme_url})\n\n" 192 return section
193
[docs] 194 def render_summary_table(self, overview_data: list) -> str: 195 """ 196 Render the summary table for the grouped overview data using the default render_table logic (no rowmod), after preprocessing with sort_overview and using od.model_dump(). 197 """ 198 # Preprocess and sort overview data as in Quarto 199 sorted_data = sort_overview(overview_data, self.table_config) 200 table_data = [] 201 for od in sorted_data: 202 p = od.project 203 row = od.model_dump() # Use model_dump to preserve all legacy field logic 204 group_dir_name = od.group.path or od.group.name.replace(" ", "-").replace( 205 "/", "-" 206 ) 207 pname = p.path or p.name.replace(" ", "-").replace("/", "-") 208 link = f"./{group_dir_name}/{pname}.md" 209 row["repo"] = f"[{p.name}]({link})" 210 if p.avatar_url: 211 row["repo"] = f"![Avatar]({p.avatar_url}){{width=16px}} {row['repo']}" 212 table_data.append((p.name, row)) 213 return render_table(table_data, self.table_config)
214 215 216__all__ = ["MarkdownRenderer"]