Coverage for lobster/tools/core/html_report/html_report.py: 84%

340 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-15 06:41 +0000

1#!/usr/bin/env python3 

2# 

3# lobster_html_report - Visualise LOBSTER report in HTML 

4# Copyright (C) 2022-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU Affero General Public License as 

8# published by the Free Software Foundation, either version 3 of the 

9# License, or (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, but 

12# WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

14# Affero General Public License for more details. 

15# 

16# You should have received a copy of the GNU Affero General Public 

17# License along with this program. If not, see 

18# <https://www.gnu.org/licenses/>. 

19import os.path 

20import argparse 

21import html 

22import subprocess 

23import hashlib 

24import tempfile 

25from datetime import datetime, timezone 

26from typing import Optional, Sequence 

27 

28import markdown 

29 

30from lobster.common.version import LOBSTER_VERSION 

31from lobster.htmldoc import htmldoc 

32from lobster.common.report import Report 

33from lobster.common.io import ensure_output_directory 

34from lobster.common.location import (Void_Reference, 

35 File_Reference, 

36 Github_Reference, 

37 Codebeamer_Reference) 

38from lobster.common.items import (Tracing_Status, Item, 

39 Requirement, Implementation, 

40 Activity) 

41from lobster.common.meta_data_tool_base import MetaDataToolBase 

42from lobster.common.graphviz_utils import is_dot_available 

43from lobster.tools.core.html_report.html_report_css import CSS 

44from lobster.tools.core.html_report.html_report_js import JAVA_SCRIPT 

45 

46 

47LOBSTER_GH = "https://github.com/bmw-software-engineering/lobster" 

48 

49 

50def name_hash(name): 

51 hobj = hashlib.md5() 

52 hobj.update(name.encode("UTF-8")) 

53 return hobj.hexdigest() 

54 

55 

56def xref_item(item, link=True, brief=False): 

57 assert isinstance(item, Item) 

58 assert isinstance(link, bool) 

59 assert isinstance(brief, bool) 

60 

61 if brief: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true

62 rv = "" 

63 elif isinstance(item, Requirement): 

64 rv = html.escape(item.framework + " " + 

65 item.kind.capitalize()) 

66 elif isinstance(item, Implementation): 

67 rv = html.escape(item.language + " " + 

68 item.kind.capitalize()) 

69 else: 

70 assert isinstance(item, Activity) 

71 rv = html.escape(item.framework + " " + 

72 item.kind.capitalize()) 

73 if not brief: 73 ↛ 76line 73 didn't jump to line 76 because the condition on line 73 was always true

74 rv += " " 

75 

76 if link: 

77 rv += f"<a href='#item-{item.tag.hash()}'>{html.escape(item.name)}</a>" 

78 else: 

79 rv += html.escape(item.name) 

80 

81 return rv 

82 

83 

84def create_policy_diagram(doc, report, dot): 

85 assert isinstance(doc, htmldoc.Document) 

86 assert isinstance(report, Report) 

87 

88 graph = 'digraph "LOBSTER Tracing Policy" {\n' 

89 for level in report.config.values(): 

90 if level.kind == "requirements": 

91 style = 'shape=box, style=rounded' 

92 elif level.kind == "implementation": 

93 style = 'shape=box' 

94 else: 

95 assert level.kind == "activity" 

96 style = 'shape=hexagon' 

97 style += f', href="#sec-{name_hash(level.name)}"' 

98 

99 graph += f' n_{name_hash(level.name)} [label="{level.name}", {style}];\n' 

100 

101 for level in report.config.values(): 

102 source = name_hash(level.name) 

103 for target in map(name_hash, level.traces): 

104 # Not a mistake; we want to show the tracing down, whereas 

105 # in the config file we indicate how we trace up. 

106 graph += f' n_{target} -> n_{source};\n' 

107 graph += "}\n" 

108 

109 with tempfile.TemporaryDirectory() as tmp_dir: 

110 graph_name = os.path.join(tmp_dir, "graph.dot") 

111 with open(graph_name, "w", encoding="UTF-8") as tmp_fd: 

112 tmp_fd.write(graph) 

113 svg = subprocess.run([dot if dot else "dot", "-Tsvg", graph_name], 

114 stdout=subprocess.PIPE, 

115 encoding="UTF-8", 

116 check=True) 

117 assert svg.returncode == 0 

118 image = svg.stdout[svg.stdout.index("<svg "):] 

119 

120 for line in image.splitlines(): 

121 doc.add_line(line) 

122 

123 

124def create_item_coverage(doc, report): 

125 assert isinstance(doc, htmldoc.Document) 

126 assert isinstance(report, Report) 

127 

128 doc.add_line("<table>") 

129 doc.add_line("<thead><tr>") 

130 doc.add_line("<td>Category</td>") 

131 doc.add_line("<td>Ratio</td>") 

132 doc.add_line("<td>Coverage</td>") 

133 doc.add_line("<td>OK Items</td>") 

134 doc.add_line("<td>Total Items</td>") 

135 doc.add_line("</tr><thead>") 

136 doc.add_line("<tbody>") 

137 doc.add_line("</tbody>") 

138 for level in report.config.values(): 

139 data = report.coverage[level.name] 

140 doc.add_line( 

141 f'<tr class="coverage-table-{level.name.replace(" ", "-").lower()}">' 

142 ) 

143 doc.add_line( 

144 f'<td><a href="#sec-{name_hash(level.name)}">' 

145 f'{html.escape(level.name)}</a></td>' 

146 ) 

147 doc.add_line(f"<td>{data.coverage:.1f}%</td>") 

148 doc.add_line("<td>") 

149 doc.add_line(f'<progress value="{data.ok}" max="{data.items}">') 

150 doc.add_line(f"{data.coverage:.2f}%") 

151 doc.add_line('</progress>') 

152 doc.add_line("</td>") 

153 doc.add_line(f'<td align="right">{data.ok}</td>') 

154 doc.add_line(f'<td align="right">{data.items}</td>') 

155 doc.add_line("</tr>") 

156 doc.add_line("</table>") 

157 

158 

159def run_git_show(commit_hash, path=None): 

160 """Run `git show` command to get the commit timestamp.""" 

161 cmd = ['git'] + (['-C', path] if path else []) + [ 

162 'show', '-s', '--format=%ct', commit_hash] 

163 try: 

164 output = subprocess.run(cmd, capture_output=True, text=True, check=True) 

165 if output.stdout.strip(): 165 ↛ 171line 165 didn't jump to line 171 because the condition on line 165 was always true

166 epoch = int(output.stdout.strip()) 

167 return str(datetime.fromtimestamp(epoch, tz=timezone.utc)) + " UTC" 

168 except subprocess.CalledProcessError: 

169 location = f"submodule path: {path}" if path else "main repository" 

170 print(f"[Warning] Could not find commit {commit_hash} in {location}.") 

171 return None 

172 

173 

174def get_commit_timestamp_utc(commit_hash, submodule_path=None): 

175 """Get commit timestamp in UTC format, either from main repo or submodule.""" 

176 timestamp = run_git_show(commit_hash) 

177 if timestamp: 177 ↛ 180line 177 didn't jump to line 180 because the condition on line 177 was always true

178 return f"{timestamp}" 

179 

180 if submodule_path: 

181 timestamp = run_git_show(commit_hash, submodule_path) 

182 if timestamp: 

183 return f"{timestamp} (from submodule at {submodule_path})" 

184 

185 return "Unknown" 

186 

187 

188def write_item_box_begin(doc, item, report): 

189 assert isinstance(doc, htmldoc.Document) 

190 assert isinstance(item, Item) 

191 

192 doc.add_line(f'<!-- begin item {html.escape(item.tag.key())} -->') 

193 

194 doc.add_line(f'<div class="item-{html.escape(item.tracing_status.name.lower())}" ' 

195 f'id="item-{item.tag.hash()}">') 

196 

197 svg_icon = ( 

198 '<svg class="icon"><use href="#svg-check-square"></use></svg>' 

199 if item.tracing_status in (Tracing_Status.OK, Tracing_Status.JUSTIFIED) 

200 else '<svg class="icon"><use href="#svg-alert-triangle"></use></svg>' 

201 ) 

202 item_div_content = f'{svg_icon} {xref_item(item, link=False)}' 

203 doc.add_line(f'<div class="item-name">{item_div_content}</div>') 

204 

205 doc.add_line('<div class="attribute">Source: ') 

206 doc.add_line('<svg class="icon"><use href="#svg-external-link"></use></svg>') 

207 

208 doc.add_line(item.location.to_html(source_root=report.source_root)) 

209 doc.add_line("</div>") 

210 

211 

212def write_item_tracing(doc, report, item): 

213 assert isinstance(doc, htmldoc.Document) 

214 assert isinstance(report, Report) 

215 assert isinstance(item, Item) 

216 

217 doc.add_line('<div class="attribute">') 

218 if item.ref_down: 

219 doc.add_line("<div>Traces to:") 

220 doc.add_line("<ul>") 

221 for ref in item.ref_down: 

222 doc.add_line(f"<li>{xref_item(report.items[ref.key()])}</li>") 

223 doc.add_line("</ul>") 

224 doc.add_line("</div>") 

225 if item.ref_up: 

226 doc.add_line("<div>Derived from:") 

227 doc.add_line("<ul>") 

228 for ref in item.ref_up: 

229 doc.add_line(f"<li>{xref_item(report.items[ref.key()])}</li>") 

230 doc.add_line("</ul>") 

231 doc.add_line("</div>") 

232 

233 if item.tracing_status == Tracing_Status.JUSTIFIED: 

234 doc.add_line("<div>Justifications:") 

235 doc.add_line("<ul>") 

236 for msg in item.just_global + item.just_up + item.just_down: 

237 doc.add_line(f"<li>{html.escape(msg)}</li>") 

238 doc.add_line("</ul>") 

239 doc.add_line("</div>") 

240 

241 if item.messages: 

242 doc.add_line("<div>Issues:") 

243 doc.add_line("<ul>") 

244 for msg in item.messages: 

245 doc.add_line(f"<li>{html.escape(msg)}</li>") 

246 doc.add_line("</ul>") 

247 doc.add_line("</div>") 

248 

249 doc.add_line("</div>") 

250 

251 

252def write_item_box_end(doc, item): 

253 assert isinstance(doc, htmldoc.Document) 

254 

255 if getattr(item.location, "commit", None) is not None: 

256 commit_hash = item.location.commit 

257 timestamp = get_commit_timestamp_utc(commit_hash, item.location.gh_repo) 

258 doc.add_line( 

259 f'<div class="attribute">' 

260 f'Build Reference: <strong>{commit_hash}</strong> | ' 

261 f'Timestamp: {timestamp}' 

262 f'</div>' 

263 ) 

264 doc.add_line("</div>") 

265 doc.add_line('<!-- end item -->') 

266 

267 

268def generate_custom_data(report) -> str: 

269 content = [ 

270 f"{key}: {value}<br>" 

271 for key, value in report.custom_data.items() 

272 if value 

273 ] 

274 return "".join(content) 

275 

276 

277def write_html_to_file(html_content: str, output_path: str) -> None: 

278 """Write HTML content to file, creating parent directories if needed.""" 

279 ensure_output_directory(output_path) 

280 with open(output_path, "w", encoding="UTF-8") as fd: 

281 fd.write(html_content) 

282 fd.write("\n") 

283 

284 

285def write_html(report, dot, high_contrast, render_md) -> str: 

286 assert isinstance(report, Report) 

287 

288 doc = htmldoc.Document( 

289 "L.O.B.S.T.E.R.", 

290 "Lightweight Open BMW Software Traceability Evidence Report" 

291 ) 

292 

293 # Item styles 

294 doc.style["#custom-data-banner"] = { 

295 "position": "absolute", 

296 "top": "1em", 

297 "right": "2em", 

298 "font-size": "0.9em", 

299 "color": "white", 

300 } 

301 doc.style[".item-ok, .item-partial, .item-missing, .item-justified"] = { 

302 "border" : "1px solid black", 

303 "border-radius" : "0.5em", 

304 "margin-top" : "0.4em", 

305 "padding" : "0.25em", 

306 } 

307 doc.style[".item-ok:target, " 

308 ".item-partial:target, " 

309 ".item-missing:target, " 

310 ".item-justified:target"] = { 

311 "border" : "3px solid black", 

312 } 

313 doc.style[".subtle-ok, " 

314 ".subtle-partial, " 

315 ".subtle-missing, " 

316 ".subtle-justified"] = { 

317 "padding-left" : "0.2em", 

318 } 

319 doc.style[".item-ok"] = { 

320 "background-color" : "#b2e1b2" if high_contrast else "#efe", 

321 } 

322 doc.style[".item-partial"] = { 

323 "background-color" : "#ffe", 

324 } 

325 doc.style[".item-missing"] = { 

326 "background-color" : "#ffb2ff" if high_contrast else "#fee", 

327 } 

328 doc.style[".item-justified"] = { 

329 "background-color" : "#eee", 

330 } 

331 doc.style[".subtle-ok"] = { 

332 "border-left" : "0.2em solid #8f8", 

333 } 

334 doc.style[".subtle-partial"] = { 

335 "border-left" : "0.2em solid #ff8", 

336 } 

337 doc.style[".subtle-missing"] = { 

338 "border-left" : "0.2em solid #f88", 

339 } 

340 doc.style[".subtle-justified"] = { 

341 "border-left" : "0.2em solid #888", 

342 } 

343 doc.style[".item-name"] = { 

344 "font-size" : "125%", 

345 "font-weight" : "bold", 

346 } 

347 doc.style[".attribute"] = { 

348 "margin-top" : "0.5em", 

349 } 

350 

351 # Render MD 

352 if render_md: 

353 doc.style[".md_description"] = { 

354 "font-style" : "unset", 

355 } 

356 doc.style[".md_description h1"] = { 

357 "padding" : "unset", 

358 "margin" : "unset" 

359 } 

360 doc.style[".md_description h2"] = { 

361 "padding" : "unset", 

362 "margin" : "unset", 

363 "border-bottom" : "unset", 

364 "text-align" : "unset" 

365 } 

366 

367 # Columns 

368 doc.style[".columns"] = { 

369 "display" : "flex", 

370 } 

371 doc.style[".columns .column"] = { 

372 "flex" : "45%", 

373 } 

374 

375 # Tables 

376 doc.style["thead tr"] = { 

377 "font-weight" : "bold", 

378 } 

379 doc.style["tbody tr.alt"] = { 

380 "background-color" : "#eee", 

381 } 

382 

383 # Text 

384 doc.style["blockquote"] = { 

385 "font-style" : "italic", 

386 "border-left" : "0.2em solid gray", 

387 "padding-left" : "0.4em", 

388 "margin-left" : "0.5em", 

389 } 

390 

391 # Footer 

392 doc.style["footer"] = { 

393 "margin-top" : "1rem", 

394 "padding" : ".2rem", 

395 "text-align" : "right", 

396 "color" : "#666", 

397 "font-size" : ".7rem", 

398 } 

399 

400 ### Menu & Navigation 

401 doc.navbar.add_link("Overview", "#sec-overview") 

402 doc.navbar.add_link("Issues", "#sec-issues") 

403 menu = doc.navbar.add_dropdown("Detailed report") 

404 for level in report.config.values(): 

405 menu.add_link(level.name, "#sec-" + name_hash(level.name)) 

406 # doc.navbar.add_link("Software Traceability Matrix", "#matrix") 

407 if report.custom_data: 

408 content = generate_custom_data(report) 

409 doc.add_line(f'<div id="custom-data-banner">{content}</div>') 

410 menu = doc.navbar.add_dropdown("LOBSTER", "right") 

411 menu.add_link("Documentation", 

412 f"{LOBSTER_GH}/blob/main/README.md") 

413 menu.add_link("License", 

414 f"{LOBSTER_GH}/blob/main/LICENSE.md") 

415 menu.add_link("Source", LOBSTER_GH) 

416 

417 ### Summary (Coverage & Policy) 

418 doc.add_heading(2, "Overview", "overview", html_identifier=True) 

419 doc.add_line('<div class="columns">') 

420 doc.add_line('<div class="column">') 

421 doc.add_heading(3, "Coverage", html_identifier=True) 

422 create_item_coverage(doc, report) 

423 doc.add_line('</div>') 

424 if is_dot_available(dot): 424 ↛ 425line 424 didn't jump to line 425 because the condition on line 424 was never true

425 doc.add_line('<div class="column">') 

426 doc.add_heading(3, "Tracing policy") 

427 create_policy_diagram(doc, report, dot) 

428 doc.add_line('</div>') 

429 else: 

430 print("warning: dot utility not found, report will not " 

431 "include the tracing policy visualisation") 

432 print("> please install Graphviz (https://graphviz.org)") 

433 doc.add_line('</div>') 

434 

435 ### Filtering 

436 doc.add_heading(2, "Filtering", "filtering-options", html_identifier=True) 

437 doc.add_heading(3, "Item Filters", html_identifier=True) 

438 doc.add_line('<div id = "btnFilterItem">') 

439 doc.add_line('<button class="button buttonAll buttonActive" ' 

440 'onclick="buttonFilter(\'all\')"> Show All </button>') 

441 

442 doc.add_line('<button class ="button buttonOK" ' 

443 'onclick="buttonFilter(\'ok\')" > OK </button>') 

444 

445 doc.add_line('<button class ="button buttonMissing" ' 

446 'onclick="buttonFilter(\'missing\')" > Missing </button>') 

447 

448 doc.add_line('<button class ="button buttonPartial" ' 

449 'onclick="buttonFilter(\'partial\')" > Partial </button>') 

450 

451 doc.add_line('<button class ="button buttonJustified" ' 

452 'onclick="buttonFilter(\'justified\')" > Justified </button>') 

453 

454 doc.add_line('<button class ="button buttonWarning" ' 

455 'onclick="buttonFilter(\'warning\')" > Warning </button>') 

456 doc.add_line("</div>") 

457 

458 doc.add_heading(3, "Show Issues", html_identifier=True) 

459 doc.add_line('<div id = "ContainerBtnToggleIssue">') 

460 doc.add_line('<button class ="button buttonBlue" id="BtnToggleIssue" ' 

461 'onclick="ToggleIssues()"> Show Issues </button>') 

462 doc.add_line('</div>') 

463 

464 doc.add_heading(3, "Filter", "filter", html_identifier=True) 

465 doc.add_line('<input type="text" id="search" placeholder="Filter..." ' 

466 'onkeyup="searchItem()">') 

467 doc.add_line('<div id="search-sec-id"') 

468 

469 ### Issues 

470 doc.add_heading(2, "Issues", "issues", html_identifier=True) 

471 doc.add_line('<div id="issues-section" style="display:none">') 

472 has_issues = False 

473 for item in sorted(report.items.values(), 

474 key = lambda x: x.location.sorting_key()): 

475 if item.tracing_status not in (Tracing_Status.OK, 

476 Tracing_Status.JUSTIFIED): 

477 for message in item.messages: 

478 if not has_issues: 

479 has_issues = True 

480 doc.add_line("<ul>") 

481 doc.add_line( 

482 f'<li class="issue issue-{item.tracing_status.name.lower()}' 

483 f' issue-{item.tracing_status.name.lower()}-' 

484 f'{item.tag.namespace}">{xref_item(item)}: {message}</li>' 

485 ) 

486 if has_issues: 

487 doc.add_line("</ul>") 

488 else: 

489 doc.add_line("<div>No traceability issues found.</div>") 

490 doc.add_line("</div>") 

491 

492 ### Report 

493 file_heading = None 

494 doc.add_heading(2, "Detailed report", "detailed-report", html_identifier=True) 

495 items_by_level = {} 

496 for level in report.config: 

497 items_by_level[level] = [item 

498 for item in report.items.values() 

499 if item.level == level] 

500 for kind, title in [("requirements", 

501 "Requirements and Specification"), 

502 ("implementation", 

503 "Implementation"), 

504 ("activity", 

505 "Verification and Validation")]: 

506 doc.add_line(f'<div class="detailed-report-{title.lower().replace(" ", "-")}">') 

507 doc.add_heading(3, title, html_identifier=True) 

508 for level in report.config.values(): 

509 if level.kind != kind: 

510 continue 

511 doc.add_line(f'<div id="section-{level.name.lower().replace(" ", "-")}">') 

512 doc.add_heading(4, 

513 html.escape(level.name), 

514 name_hash(level.name), 

515 html_identifier=True, 

516 ) 

517 if items_by_level[level.name]: 517 ↛ 556line 517 didn't jump to line 556 because the condition on line 517 was always true

518 for item in sorted(items_by_level[level.name], 

519 key = lambda x: x.location.sorting_key()): 

520 if isinstance(item.location, Void_Reference): 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true

521 new_file_heading = "Unknown" 

522 elif isinstance(item.location, (File_Reference, 

523 Github_Reference)): 

524 new_file_heading = item.location.filename 

525 elif isinstance(item.location, Codebeamer_Reference): 

526 new_file_heading = ( 

527 f"Codebeamer {item.location.cb_root}," 

528 f" tracker {item.location.tracker}" 

529 ) 

530 else: # pragma: no cover 

531 assert False 

532 if new_file_heading != file_heading: 

533 file_heading = new_file_heading 

534 doc.add_heading(5, html.escape(file_heading)) 

535 

536 write_item_box_begin(doc, item, report) 

537 if isinstance(item, Requirement) and item.status: 

538 doc.add_line('<div class="attribute">') 

539 doc.add_line(f"Status: {html.escape(item.status)}") 

540 doc.add_line('</div>') 

541 if (isinstance(item, (Requirement, Activity)) and item.text): 

542 if render_md: 

543 bq_class = ' class="md_description"' 

544 bq_text = markdown.markdown(item.text, 

545 extensions=['tables']) 

546 else: 

547 bq_class = "" 

548 bq_text = html.escape(item.text).replace("\n", "<br>") 

549 

550 doc.add_line('<div class="attribute">') 

551 doc.add_line(f"<blockquote{bq_class}>{bq_text}</blockquote>") 

552 doc.add_line('</div>') 

553 write_item_tracing(doc, report, item) 

554 write_item_box_end(doc, item) 

555 else: 

556 doc.add_line("No items recorded at this level.") 

557 doc.add_line("</div>") # Closing tag for id #level.name 

558 doc.add_line("</div>") # Closing tag for detailed-report-<title> 

559 # Closing tag for id #search-sec-id 

560 doc.add_line("</div>") 

561 # Add LOBSTER version in the footer. 

562 doc.add_line("<footer>") 

563 doc.add_line(f"<p>LOBSTER Version: {LOBSTER_VERSION}</p>") 

564 doc.add_line("</footer>") 

565 

566 # Add the css from assets 

567 doc.css.append(CSS.lstrip()) 

568 

569 # Add javascript from assets/html_report.js file 

570 doc.scripts.append(JAVA_SCRIPT.lstrip()) 

571 

572 return doc.render() 

573 

574 

575class HtmlReportTool(MetaDataToolBase): 

576 def __init__(self): 

577 super().__init__( 

578 name="html-report", 

579 description="Visualise LOBSTER report in HTML", 

580 official=True, 

581 ) 

582 

583 ap = self._argument_parser 

584 ap.add_argument("lobster_report", 

585 nargs="?", 

586 default="report.lobster") 

587 ap.add_argument("--out", 

588 default="lobster_report.html") 

589 ap.add_argument("--dot", 

590 help="path to dot utility (https://graphviz.org), \ 

591 by default expected in PATH", 

592 default=None) 

593 ap.add_argument("--high-contrast", 

594 action="store_true", 

595 help="Uses a color palette with a higher contrast.") 

596 ap.add_argument("--render-md", 

597 action="store_true", 

598 help="Renders MD in description.") 

599 ap.add_argument("--source-root", 

600 default="", 

601 help="Prefix to prepend to file reference links, " 

602 "e.g. a path from the HTML output location " 

603 "back to the workspace root.") 

604 

605 def _run_impl(self, options: argparse.Namespace) -> int: 

606 if not os.path.isfile(options.lobster_report): 

607 self._argument_parser.error(f"{options.lobster_report} is not a file") 

608 

609 report = Report() 

610 report.load_report(options.lobster_report) 

611 report.source_root = options.source_root 

612 

613 html_content = write_html( 

614 report = report, 

615 dot = options.dot, 

616 high_contrast = options.high_contrast, 

617 render_md = options.render_md, 

618 ) 

619 write_html_to_file(html_content, options.out) 

620 print(f"LOBSTER HTML report written to {options.out}") 

621 

622 return 0 

623 

624 

625def lobster_html_report( 

626 lobster_report_path: str, 

627 output_html_path: str, 

628 dot_path: str = None, 

629 high_contrast: bool = False, 

630 render_md: bool = False, 

631 source_root: str = "", 

632) -> None: 

633 """ 

634 API function to generate an HTML report from a LOBSTER report file. 

635 

636 Args: 

637 lobster_report_path (str): Path to the input LOBSTER report file. 

638 output_html_path (str): Path to the output HTML file. 

639 dot_path (str, optional): Path to the Graphviz 'dot' utility. 

640 high_contrast (bool, optional): Use high contrast colors. 

641 render_md (bool, optional): Render Markdown in descriptions. 

642 source_root (str, optional): Prefix to prepend to file reference links. 

643 """ 

644 report = Report() 

645 report.load_report(lobster_report_path) 

646 report.source_root = source_root 

647 html_content = write_html( 

648 report=report, 

649 dot=dot_path, 

650 high_contrast=high_contrast, 

651 render_md=render_md, 

652 ) 

653 write_html_to_file(html_content, output_html_path) 

654 

655 

656def main(args: Optional[Sequence[str]] = None) -> int: 

657 return HtmlReportTool().run(args)