Coverage for src / competitive_verifier / documents / render.py: 94%

342 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-06-05 08:59 +0900

1import datetime 

2import enum 

3import pathlib 

4from abc import ABC, abstractmethod 

5from collections.abc import Iterable 

6from collections.abc import Set as AbstractSet 

7from dataclasses import dataclass 

8from functools import cached_property 

9from itertools import chain 

10from logging import getLogger 

11from typing import BinaryIO 

12 

13from pydantic import BaseModel 

14 

15from competitive_verifier import git, log 

16from competitive_verifier.models import ( 

17 DocumentOutputMode, 

18 ForcePosixPath, 

19 ProblemVerification, 

20 ResultStatus, 

21 SortedPathSet, 

22 VerificationFile, 

23 VerificationInput, 

24 VerificationResult, 

25 VerifyCommandResult, 

26) 

27from competitive_verifier.util import ( 

28 normalize_bytes_text, 

29 read_text_normalized, 

30 resolve_referenced_path, 

31) 

32 

33from .config import ConfigYaml 

34from .front_matter import FrontMatter, Markdown 

35from .path_sort import ( 

36 PathSortOrder, 

37 is_natural_path_sort, 

38 path_sort_key_path, 

39) 

40from .render_data import ( 

41 CategorizedIndex, 

42 CodePageData, 

43 Dependency, 

44 EmbeddedCode, 

45 EnvTestcaseResult, 

46 IndexFiles, 

47 IndexRenderData, 

48 MultiCodePageData, 

49 PageRenderData, 

50 RenderLink, 

51 StatusIcon, 

52) 

53 

54logger = getLogger(__name__) 

55 

56 

57def _paths_to_render_links( 

58 paths: SortedPathSet, 

59 page_jobs: dict[pathlib.Path, "PageRenderJob"], 

60 *, 

61 path_sort: PathSortOrder | None = None, 

62) -> list[RenderLink]: 

63 def get_link(path: pathlib.Path) -> RenderLink | None: 

64 job = page_jobs.get(path) 

65 if not job: 

66 return None 

67 return job.to_render_link() 

68 

69 sorted_paths = sorted(paths, key=lambda p: path_sort_key_path(p, path_sort)) 

70 

71 return [link for link in map(get_link, sorted_paths) if link] 

72 

73 

74class MultiTargetMarkdown(Markdown): 

75 path: ForcePosixPath # pyright: ignore[reportIncompatibleVariableOverride, reportGeneralTypeIssues] 

76 front_matter: FrontMatter # pyright: ignore[reportIncompatibleVariableOverride] 

77 multi_documentation_of: list[pathlib.Path] 

78 

79 

80@dataclass 

81class UserMarkdowns: 

82 single: dict[pathlib.Path, Markdown] 

83 multi: list[MultiTargetMarkdown] 

84 

85 @staticmethod 

86 def select_markdown(sources: set[pathlib.Path]) -> "UserMarkdowns": 

87 single: dict[pathlib.Path, Markdown] = {} 

88 multi: list[MultiTargetMarkdown] = [] 

89 markdowns = [Markdown.load_file(t) for t in sources if t.suffix == ".md"] 

90 for md in markdowns: 

91 if not (md.path and md.front_matter and md.front_matter.documentation_of): 

92 continue 

93 

94 if isinstance(md.front_matter.documentation_of, str): 

95 source_path = resolve_referenced_path( 

96 md.front_matter.documentation_of, 

97 basedir=md.path.parent, 

98 ) 

99 if source_path in sources: 

100 md.front_matter.documentation_of = source_path.as_posix() 

101 single[source_path] = md 

102 else: 

103 logger.warning( 

104 "Markdown(%s) documentation_of: %s is not found.", 

105 md.path, 

106 md.front_matter.documentation_of, 

107 extra={"github": log.GitHubMessageParams(file=md.path)}, 

108 ) 

109 else: 

110 multi_documentation_of: list[pathlib.Path] = [] 

111 for d in md.front_matter.documentation_of: 

112 source_path = resolve_referenced_path( 

113 d, 

114 basedir=md.path.parent, 

115 ) 

116 if source_path in sources: 116 ↛ 119line 116 didn't jump to line 119 because the condition on line 116 was always true

117 multi_documentation_of.append(source_path) 

118 else: 

119 logger.warning( 

120 "Markdown(%s) documentation_of: %s is not found.", 

121 md.path, 

122 d, 

123 extra={"github": log.GitHubMessageParams(file=md.path)}, 

124 ) 

125 if multi_documentation_of: 125 ↛ 135line 125 didn't jump to line 135 because the condition on line 125 was always true

126 multi.append( 

127 MultiTargetMarkdown( 

128 path=md.path, 

129 front_matter=md.front_matter, 

130 content=md.content, 

131 multi_documentation_of=multi_documentation_of, 

132 ) 

133 ) 

134 else: 

135 logger.warning( 

136 "Markdown(%s) documentation_of have no valid files.", 

137 md.path, 

138 extra={"github": log.GitHubMessageParams(file=md.path)}, 

139 ) 

140 

141 for m in multi: 

142 if m.front_matter and m.front_matter.keep_single: 

143 continue 

144 for source in m.multi_documentation_of: 

145 redirect_to = f"/{m.path.with_suffix('').as_posix()}" 

146 s = single.get(source) 

147 if not s: 

148 s = Markdown(content=b"", front_matter=None) 

149 if not s.front_matter: 

150 s.front_matter = FrontMatter() 

151 

152 if m.front_matter.display == DocumentOutputMode.never: 

153 s.front_matter.display = DocumentOutputMode.never 

154 else: 

155 s.front_matter.display = DocumentOutputMode.no_index 

156 s.front_matter.redirect_to = redirect_to 

157 

158 single[source] = s 

159 return UserMarkdowns( 

160 single=single, 

161 multi=multi, 

162 ) 

163 

164 

165class _VerificationStatusFlag(enum.Flag): 

166 IS_LIBRARY = 0 

167 NOTHING = 0 

168 LIBRARY_NOTHING = IS_LIBRARY 

169 TEST_NOTHING = enum.auto() 

170 IS_TEST = TEST_NOTHING 

171 HAVE_AC = enum.auto() 

172 HAVE_WA = enum.auto() 

173 HAVE_SKIP = enum.auto() 

174 

175 LIBRARY_AC_WA_SKIP = IS_LIBRARY | HAVE_AC | HAVE_WA | HAVE_SKIP 

176 LIBRARY_AC_WA = IS_LIBRARY | HAVE_AC | HAVE_WA 

177 LIBRARY_AC_SKIP = IS_LIBRARY | HAVE_AC | HAVE_SKIP 

178 LIBRARY_AC = IS_LIBRARY | HAVE_AC 

179 LIBRARY_WA_SKIP = IS_LIBRARY | HAVE_WA | HAVE_SKIP 

180 LIBRARY_WA = IS_LIBRARY | HAVE_WA 

181 LIBRARY_SKIP = IS_LIBRARY | HAVE_SKIP 

182 

183 TEST_AC_WA_SKIP = IS_TEST | HAVE_AC | HAVE_WA | HAVE_SKIP 

184 TEST_AC_WA = IS_TEST | HAVE_AC | HAVE_WA 

185 TEST_AC_SKIP = IS_TEST | HAVE_AC | HAVE_SKIP 

186 TEST_AC = IS_TEST | HAVE_AC 

187 TEST_WA_SKIP = IS_TEST | HAVE_WA | HAVE_SKIP 

188 TEST_WA = IS_TEST | HAVE_WA 

189 TEST_SKIP = IS_TEST | HAVE_SKIP 

190 

191 def to_status(self) -> StatusIcon: 

192 d = { 

193 self.LIBRARY_AC_WA_SKIP: StatusIcon.LIBRARY_SOME_WA, 

194 self.LIBRARY_AC_WA: StatusIcon.LIBRARY_SOME_WA, 

195 self.LIBRARY_AC_SKIP: StatusIcon.LIBRARY_PARTIAL_AC, 

196 self.LIBRARY_AC: StatusIcon.LIBRARY_ALL_AC, 

197 self.LIBRARY_WA_SKIP: StatusIcon.LIBRARY_ALL_WA, 

198 self.LIBRARY_WA: StatusIcon.LIBRARY_ALL_WA, 

199 self.LIBRARY_SKIP: StatusIcon.LIBRARY_NO_TESTS, 

200 self.LIBRARY_NOTHING: StatusIcon.LIBRARY_NO_TESTS, 

201 self.TEST_AC_WA_SKIP: StatusIcon.TEST_WRONG_ANSWER, 

202 self.TEST_AC_WA: StatusIcon.TEST_WRONG_ANSWER, 

203 self.TEST_AC_SKIP: StatusIcon.TEST_WAITING_JUDGE, 

204 self.TEST_AC: StatusIcon.TEST_ACCEPTED, 

205 self.TEST_WA_SKIP: StatusIcon.TEST_WRONG_ANSWER, 

206 self.TEST_WA: StatusIcon.TEST_WRONG_ANSWER, 

207 self.TEST_SKIP: StatusIcon.TEST_WAITING_JUDGE, 

208 self.TEST_NOTHING: StatusIcon.TEST_WAITING_JUDGE, 

209 } 

210 return d[self] 

211 

212 @classmethod 

213 def from_status(cls, status: StatusIcon) -> "_VerificationStatusFlag": 

214 d = { 

215 StatusIcon.LIBRARY_SOME_WA: cls.LIBRARY_AC_WA, 

216 StatusIcon.LIBRARY_PARTIAL_AC: cls.LIBRARY_AC_SKIP, 

217 StatusIcon.LIBRARY_ALL_AC: cls.LIBRARY_AC, 

218 StatusIcon.LIBRARY_ALL_WA: cls.LIBRARY_WA, 

219 StatusIcon.LIBRARY_NO_TESTS: cls.LIBRARY_NOTHING, 

220 StatusIcon.TEST_ACCEPTED: cls.TEST_AC, 

221 StatusIcon.TEST_WRONG_ANSWER: cls.TEST_WA, 

222 StatusIcon.TEST_WAITING_JUDGE: cls.TEST_NOTHING, 

223 } 

224 return d[status] 

225 

226 

227class SourceCodeStat(BaseModel): 

228 path: ForcePosixPath 

229 is_verification: bool 

230 verification_status: StatusIcon 

231 file_input: VerificationFile 

232 timestamp: datetime.datetime 

233 depends_on: SortedPathSet 

234 required_by: SortedPathSet 

235 verified_with: SortedPathSet 

236 verification_results: list[VerificationResult] | None = None 

237 

238 @staticmethod 

239 def resolve_dependency( 

240 *, 

241 verifications: VerificationInput, 

242 result: VerifyCommandResult, 

243 included_files: AbstractSet[pathlib.Path], 

244 ) -> dict[pathlib.Path, "SourceCodeStat"]: 

245 d: dict[pathlib.Path, SourceCodeStat] = {} 

246 statuses: dict[pathlib.Path, _VerificationStatusFlag] = dict.fromkeys( 

247 verifications.files.keys(), _VerificationStatusFlag.NOTHING 

248 ) 

249 verification_results_dict: dict[pathlib.Path, list[VerificationResult]] = {} 

250 

251 for p, r in result.files.items(): 

252 if p not in included_files: 252 ↛ 253line 252 didn't jump to line 253 because the condition on line 252 was never true

253 continue 

254 st = _VerificationStatusFlag.NOTHING 

255 for v in r.verifications: 

256 if v.status == ResultStatus.SUCCESS: 

257 st |= _VerificationStatusFlag.HAVE_AC 

258 elif v.status == ResultStatus.FAILURE: 

259 st |= _VerificationStatusFlag.HAVE_WA 

260 elif v.status == ResultStatus.SKIPPED: 260 ↛ 255line 260 didn't jump to line 255 because the condition on line 260 was always true

261 st |= _VerificationStatusFlag.HAVE_SKIP 

262 statuses[p] = st 

263 verification_results_dict[p] = r.verifications 

264 

265 for group0 in verifications.scc(): 

266 group = group0 & included_files 

267 if not group: 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true

268 continue 

269 

270 group_status = _VerificationStatusFlag.NOTHING 

271 

272 for path in group: 

273 group_status |= statuses[path] 

274 

275 for path in group: 

276 depends_on = verifications.depends_on[path] & included_files 

277 required_by = verifications.required_by[path] & included_files 

278 verified_with = verifications.verified_with[path] & included_files 

279 

280 for dep in depends_on: 

281 statuses[dep] |= group_status 

282 

283 timestamp = git.get_commit_time( 

284 verifications.transitive_depends_on[path] 

285 ) 

286 file_input = verifications.files[path] 

287 is_verification = file_input.is_verification() 

288 

289 verification_results = verification_results_dict.get(path) 

290 

291 if is_verification and verification_results is None: 291 ↛ 292line 291 didn't jump to line 292 because the condition on line 291 was never true

292 raise ValueError("needs verification_results") 

293 

294 flag_status = group_status | ( 

295 _VerificationStatusFlag.IS_TEST 

296 if is_verification 

297 else _VerificationStatusFlag.IS_LIBRARY 

298 ) 

299 

300 d[path] = SourceCodeStat( 

301 path=path, 

302 file_input=file_input, 

303 is_verification=is_verification, 

304 depends_on=depends_on, 

305 required_by=required_by, 

306 verified_with=verified_with, 

307 timestamp=timestamp, 

308 verification_status=flag_status.to_status(), 

309 verification_results=verification_results, 

310 ) 

311 return d 

312 

313 

314class RenderJob(ABC): 

315 @property 

316 @abstractmethod 

317 def destination_name(self) -> pathlib.Path: ... 

318 

319 @abstractmethod 

320 def write_to(self, fp: BinaryIO): ... 

321 

322 @staticmethod 

323 def enumerate_jobs( 

324 *, 

325 sources: set[pathlib.Path], 

326 verifications: VerificationInput, 

327 result: VerifyCommandResult, 

328 config: ConfigYaml, 

329 index_md: Markdown | None = None, 

330 ) -> list["RenderJob"]: 

331 def plain_content(source: pathlib.Path) -> RenderJob | None: 

332 if source.suffix == ".md": 

333 md = Markdown.load_file(source) 

334 if md.front_matter and md.front_matter.documentation_of: 

335 return None 

336 elif source.suffix == ".html": 

337 pass 

338 else: 

339 return None 

340 return PlainRenderJob( 

341 source_path=source, 

342 content=source.read_bytes(), 

343 ) 

344 

345 user_markdowns = UserMarkdowns.select_markdown(sources) 

346 

347 logger.info(" %s source files...", len(sources)) 

348 

349 class SourceForDebug(BaseModel): 

350 sources: SortedPathSet 

351 markdowns: UserMarkdowns 

352 

353 logger.debug( 

354 "source: %s", 

355 SourceForDebug( 

356 sources=sources, 

357 markdowns=user_markdowns, 

358 ), 

359 ) 

360 with log.group("Resolve dependency"): 

361 stats_dict = SourceCodeStat.resolve_dependency( 

362 verifications=verifications, 

363 result=result, 

364 included_files=sources, 

365 ) 

366 

367 page_jobs: dict[pathlib.Path, PageRenderJob] = {} 

368 jobs: list[RenderJob] = [] 

369 source_iter = ( 

370 sorted(sources, key=lambda p: path_sort_key_path(p, config.path_sort)) 

371 if is_natural_path_sort(config.path_sort) 

372 else sources 

373 ) 

374 

375 for source in source_iter: 

376 markdown = user_markdowns.single.get(source) or Markdown.make_default( 

377 source 

378 ) 

379 stat = stats_dict.get(source) 

380 if not stat: 

381 plain_job = plain_content(source) 

382 if plain_job is not None: 

383 jobs.append(plain_job) 

384 elif source.suffix != ".md": 

385 logger.info("Skip file: %s", source) 

386 continue 

387 group_dir = None 

388 if config.consolidate: 

389 consolidate = config.consolidate 

390 group_dir = next( 

391 filter(lambda p: p in consolidate, source.parents), None 

392 ) 

393 

394 pj = PageRenderJob( 

395 source_path=source, 

396 group_dir=group_dir or source.parent, 

397 markdown=markdown, 

398 stat=stat, 

399 verifications=verifications, 

400 result=result, 

401 page_jobs=page_jobs, 

402 path_sort=config.path_sort, 

403 ) 

404 

405 if pj.display == DocumentOutputMode.never: 

406 continue 

407 

408 page_jobs[pj.source_path] = pj 

409 jobs.append(pj) 

410 

411 multis: list[MultiCodePageRenderJob] = [] 

412 for md in user_markdowns.multi: 

413 group_dir = None 

414 if config.consolidate: 414 ↛ 419line 414 didn't jump to line 419 because the condition on line 414 was always true

415 consolidate = config.consolidate 

416 group_dir = next( 

417 filter(lambda p: p in consolidate, md.path.parents), None 

418 ) 

419 job = MultiCodePageRenderJob( 

420 markdown=md, 

421 group_dir=group_dir or md.path.parent, 

422 page_jobs=page_jobs, 

423 path_sort=config.path_sort, 

424 ) 

425 

426 if md.front_matter.display == DocumentOutputMode.never: 

427 continue 

428 multis.append(job) 

429 

430 jobs.extend(multis) 

431 jobs.append( 

432 IndexRenderJob( 

433 page_jobs=page_jobs, 

434 multicode_docs=multis, 

435 index_md=index_md, 

436 path_sort=config.path_sort, 

437 ) 

438 ) 

439 return jobs 

440 

441 

442@dataclass(frozen=True) 

443class PlainRenderJob(RenderJob): 

444 source_path: ForcePosixPath 

445 content: bytes 

446 

447 @property 

448 def destination_name(self): 

449 return self.source_path 

450 

451 def write_to(self, fp: BinaryIO): 

452 fp.write(self.content) 

453 

454 

455class MarkdownRenderJob(RenderJob): 

456 source_path: pathlib.Path 

457 markdown: Markdown 

458 

459 @property 

460 def destination_name(self): 

461 return self.source_path 

462 

463 def write_to(self, fp: BinaryIO): 

464 self.markdown.dump_merged(fp) 

465 

466 

467@dataclass(frozen=True) 

468class PageRenderJob(RenderJob): 

469 source_path: pathlib.Path 

470 group_dir: pathlib.Path 

471 markdown: Markdown 

472 stat: SourceCodeStat 

473 verifications: VerificationInput 

474 result: VerifyCommandResult 

475 page_jobs: dict[pathlib.Path, "PageRenderJob"] 

476 path_sort: PathSortOrder | None = None 

477 

478 @property 

479 def is_verification(self): 

480 return self.stat.is_verification 

481 

482 @property 

483 def display(self): 

484 return self.front_matter.display or DocumentOutputMode.visible 

485 

486 def __str__(self) -> str: 

487 return f"PageRenderJob(source_path={self.source_path!r},markdown={self.markdown!r},stat={self.stat!r})" 

488 

489 def validate_front_matter(self): 

490 front_matter = self.markdown.front_matter 

491 if ( 491 ↛ 499line 491 didn't jump to line 499 because the condition on line 491 was never true

492 front_matter 

493 and front_matter.documentation_of 

494 and ( 

495 not isinstance(front_matter.documentation_of, str) 

496 or self.source_path != pathlib.Path(front_matter.documentation_of) 

497 ) 

498 ): 

499 raise ValueError( 

500 "PageRenderJob.path must equal front_matter.documentation_of." 

501 ) 

502 

503 def to_render_link(self, *, index: bool = False) -> RenderLink | None: 

504 if self.display in (DocumentOutputMode.hidden, DocumentOutputMode.never) or ( 

505 index and self.display == DocumentOutputMode.no_index 

506 ): 

507 return None 

508 return RenderLink( 

509 path=self.source_path, 

510 filename=self.source_path.relative_to(self.group_dir).as_posix(), 

511 title=self.front_matter.title, 

512 icon=self.stat.verification_status, 

513 ) 

514 

515 @cached_property 

516 def front_matter(self) -> FrontMatter: 

517 front_matter = ( 

518 self.markdown.front_matter.model_copy() 

519 if self.markdown.front_matter 

520 else FrontMatter() 

521 ) 

522 front_matter.documentation_of = self.source_path.as_posix() 

523 if not front_matter.layout: 523 ↛ 526line 523 didn't jump to line 526 because the condition on line 523 was always true

524 front_matter.layout = "document" 

525 

526 input_file = self.verifications.files.get(self.source_path) 

527 if not front_matter.title and (input_file and input_file.title): 

528 front_matter.title = input_file.title 

529 if not front_matter.display and (input_file and input_file.display): 

530 front_matter.display = input_file.display 

531 

532 return front_matter 

533 

534 @property 

535 def destination_name(self): 

536 return self.source_path.with_suffix(self.source_path.suffix + ".md") 

537 

538 def write_to(self, fp: BinaryIO): 

539 self.validate_front_matter() 

540 front_matter = self.front_matter 

541 front_matter.data = self.get_page_data() 

542 Markdown( 

543 path=self.source_path, 

544 front_matter=front_matter, 

545 content=self.markdown.content, 

546 ).dump_merged(fp) 

547 

548 def get_page_data(self) -> PageRenderData: 

549 depends_on = _paths_to_render_links( 

550 self.stat.depends_on, 

551 self.page_jobs, 

552 path_sort=self.path_sort, 

553 ) 

554 required_by = _paths_to_render_links( 

555 self.stat.required_by, 

556 self.page_jobs, 

557 path_sort=self.path_sort, 

558 ) 

559 verified_with = _paths_to_render_links( 

560 self.stat.verified_with, 

561 self.page_jobs, 

562 path_sort=self.path_sort, 

563 ) 

564 

565 attributes = self.stat.file_input.document_attributes.copy() 

566 if problem_url := next( 

567 ( 

568 v.problem 

569 for v in self.stat.file_input.verification_list 

570 if isinstance(v, ProblemVerification) 

571 ), 

572 None, 

573 ): 

574 attributes.setdefault("PROBLEM", problem_url) 

575 

576 code = read_text_normalized(self.source_path) 

577 

578 embedded = [EmbeddedCode(name="default", code=code)] 

579 embedded.extend( 

580 EmbeddedCode(name=s.name, code=read_text_normalized(s.path)) 

581 for s in self.stat.file_input.additional_sources 

582 ) 

583 

584 return PageRenderData( 

585 path=self.source_path, 

586 path_extension=self.source_path.suffix.lstrip("."), 

587 title=self.front_matter.title, 

588 embedded=embedded, 

589 timestamp=self.stat.timestamp, 

590 attributes=attributes, 

591 testcases=( 

592 [ 

593 EnvTestcaseResult( 

594 name=c.name, 

595 status=c.status, 

596 elapsed=c.elapsed, 

597 memory=c.memory, 

598 environment=v.verification_name, 

599 ) 

600 for v in self.stat.verification_results 

601 for c in (v.testcases or []) 

602 ] 

603 if self.stat.verification_results 

604 else None 

605 ), 

606 verification_status=self.stat.verification_status, 

607 is_verification_file=self.stat.is_verification, 

608 is_failed=self.stat.verification_status.is_failed, 

609 document_path=self.markdown.path, 

610 dependencies=[ 

611 Dependency(type="Depends on", files=depends_on), 

612 Dependency(type="Required by", files=required_by), 

613 Dependency(type="Verified with", files=verified_with), 

614 ], 

615 depends_on=[link.path for link in depends_on], 

616 required_by=[link.path for link in required_by], 

617 verified_with=[link.path for link in verified_with], 

618 ) 

619 

620 

621@dataclass(frozen=True) 

622class MultiCodePageRenderJob(RenderJob): 

623 markdown: MultiTargetMarkdown 

624 group_dir: pathlib.Path 

625 page_jobs: dict[pathlib.Path, "PageRenderJob"] 

626 path_sort: PathSortOrder | None = None 

627 

628 def __str__(self) -> str: 

629 return f"MultiCodePageRenderJob(multi_documentation_of={self.markdown.multi_documentation_of!r})" 

630 

631 @cached_property 

632 def jobs(self) -> list[PageRenderJob]: 

633 jobs: list[PageRenderJob] = [] 

634 for m in self.markdown.multi_documentation_of: 

635 job = self.page_jobs.get(m) 

636 if not job: 636 ↛ 637line 636 didn't jump to line 637 because the condition on line 636 was never true

637 continue 

638 jobs.append(job) 

639 return jobs 

640 

641 @cached_property 

642 def verification_status(self) -> StatusIcon: 

643 flag = _VerificationStatusFlag.NOTHING 

644 for job in self.jobs: 

645 flag |= _VerificationStatusFlag.from_status(job.stat.verification_status) 

646 return flag.to_status() 

647 

648 @property 

649 def is_verification(self): 

650 return self.verification_status.is_test 

651 

652 @property 

653 def display(self): 

654 return self.markdown.front_matter.display or DocumentOutputMode.visible 

655 

656 @property 

657 def destination_name(self) -> pathlib.Path: 

658 return self.markdown.path 

659 

660 def to_render_link(self, *, index: bool = False) -> RenderLink: 

661 return RenderLink( 

662 path=self.markdown.path.with_suffix(""), 

663 filename=self.markdown.path.relative_to(self.group_dir).as_posix(), 

664 title=self.markdown.front_matter.title, 

665 icon=self.verification_status, 

666 ) 

667 

668 def write_to(self, fp: BinaryIO): 

669 front_matter = self.markdown.front_matter 

670 front_matter.layout = "multidoc" 

671 front_matter.data = self.get_page_data() 

672 Markdown( 

673 path=self.markdown.path, 

674 front_matter=front_matter, 

675 content=self.markdown.content, 

676 ).dump_merged(fp) 

677 

678 def get_page_data(self) -> MultiCodePageData: 

679 codes = [ 

680 CodePageData.model_validate( 

681 {"document_content": normalize_bytes_text(j.markdown.content)} 

682 | j.get_page_data().model_dump(), 

683 ) 

684 for j in self.jobs 

685 ] 

686 

687 multi_documentation_of_set = set(self.markdown.multi_documentation_of) 

688 multi_documentation_of_set.add(self.markdown.path) 

689 depends_on_paths = ( 

690 set(chain.from_iterable(j.stat.depends_on for j in self.jobs)) 

691 - multi_documentation_of_set 

692 ) 

693 required_by_paths = ( 

694 set(chain.from_iterable(j.stat.required_by for j in self.jobs)) 

695 - multi_documentation_of_set 

696 ) 

697 verified_with_paths = ( 

698 set(chain.from_iterable(j.stat.verified_with for j in self.jobs)) 

699 - multi_documentation_of_set 

700 ) 

701 

702 depends_on = _paths_to_render_links( 

703 depends_on_paths, 

704 self.page_jobs, 

705 path_sort=self.path_sort, 

706 ) 

707 required_by = _paths_to_render_links( 

708 required_by_paths, 

709 self.page_jobs, 

710 path_sort=self.path_sort, 

711 ) 

712 verified_with = _paths_to_render_links( 

713 verified_with_paths, 

714 self.page_jobs, 

715 path_sort=self.path_sort, 

716 ) 

717 

718 return MultiCodePageData( 

719 path=self.markdown.path, 

720 verification_status=self.verification_status, 

721 is_failed=any(c.is_failed for c in codes), 

722 codes=codes, 

723 dependencies=[ 

724 Dependency(type="Depends on", files=depends_on), 

725 Dependency(type="Required by", files=required_by), 

726 Dependency(type="Verified with", files=verified_with), 

727 ], 

728 ) 

729 

730 

731@dataclass 

732class IndexRenderJob(RenderJob): 

733 page_jobs: dict[pathlib.Path, "PageRenderJob"] 

734 multicode_docs: list[MultiCodePageRenderJob] 

735 path_sort: PathSortOrder | None = None 

736 

737 index_md: Markdown | None = None 

738 

739 def __str__(self) -> str: 

740 @dataclass 

741 class _IndexRenderJob: 

742 job_paths: Iterable[pathlib.Path] 

743 

744 s = repr( 

745 _IndexRenderJob( 

746 job_paths=self.page_jobs.keys(), 

747 ) 

748 ) 

749 index = s.find("_IndexRenderJob") 

750 return s[index + 1 :] 

751 

752 @property 

753 def destination_name(self): 

754 return pathlib.Path("index.md") 

755 

756 def write_to(self, fp: BinaryIO): 

757 Markdown( 

758 path=self.destination_name, 

759 front_matter=FrontMatter( 

760 layout="toppage", 

761 data=self.get_page_data(), 

762 ), 

763 content=self.index_md.content if self.index_md else b"", 

764 ).dump_merged(fp) 

765 

766 def get_page_data(self) -> IndexRenderData: 

767 library_categories: dict[str, list[RenderLink]] = {} 

768 verification_categories: dict[str, list[RenderLink]] = {} 

769 index_jobs: Iterable[PageRenderJob | MultiCodePageRenderJob] = chain( 

770 self.page_jobs.values(), 

771 self.multicode_docs, 

772 ) 

773 for job in index_jobs: 

774 if job.display != DocumentOutputMode.visible: 

775 continue 

776 categories = ( 

777 verification_categories if job.is_verification else library_categories 

778 ) 

779 

780 directory = job.group_dir 

781 category = directory.as_posix() 

782 if category == ".": 

783 category = "" 

784 elif not category.endswith("/"): 784 ↛ 787line 784 didn't jump to line 787 because the condition on line 784 was always true

785 category = f"{category}/" 

786 

787 if category not in categories: 

788 categories[category] = [] 

789 

790 link = job.to_render_link(index=True) 

791 if link: 791 ↛ 773line 791 didn't jump to line 773 because the condition on line 791 was always true

792 categories[category].append(link) 

793 

794 def _build_categories_list( 

795 categories: dict[str, list[RenderLink]], 

796 ) -> list[CategorizedIndex]: 

797 return sorted( 

798 ( 

799 CategorizedIndex( 

800 name=category, 

801 pages=( 

802 sorted( 

803 pages, 

804 key=lambda p: path_sort_key_path( 

805 p.path, self.path_sort 

806 ), 

807 ) 

808 if is_natural_path_sort(self.path_sort) 

809 else sorted(pages, key=lambda p: p.path.as_posix()) 

810 ), 

811 ) 

812 for category, pages in categories.items() 

813 ), 

814 key=lambda d: d.name, 

815 ) 

816 

817 return IndexRenderData( 

818 top=[ 

819 IndexFiles( 

820 type="Library Files", 

821 categories=_build_categories_list(library_categories), 

822 ), 

823 IndexFiles( 

824 type="Verification Files", 

825 categories=_build_categories_list(verification_categories), 

826 ), 

827 ], 

828 )