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
« 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
13from pydantic import BaseModel
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)
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)
54logger = getLogger(__name__)
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()
69 sorted_paths = sorted(paths, key=lambda p: path_sort_key_path(p, path_sort))
71 return [link for link in map(get_link, sorted_paths) if link]
74class MultiTargetMarkdown(Markdown):
75 path: ForcePosixPath # pyright: ignore[reportIncompatibleVariableOverride, reportGeneralTypeIssues]
76 front_matter: FrontMatter # pyright: ignore[reportIncompatibleVariableOverride]
77 multi_documentation_of: list[pathlib.Path]
80@dataclass
81class UserMarkdowns:
82 single: dict[pathlib.Path, Markdown]
83 multi: list[MultiTargetMarkdown]
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
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 )
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()
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
158 single[source] = s
159 return UserMarkdowns(
160 single=single,
161 multi=multi,
162 )
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()
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
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
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]
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]
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
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]] = {}
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
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
270 group_status = _VerificationStatusFlag.NOTHING
272 for path in group:
273 group_status |= statuses[path]
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
280 for dep in depends_on:
281 statuses[dep] |= group_status
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()
289 verification_results = verification_results_dict.get(path)
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")
294 flag_status = group_status | (
295 _VerificationStatusFlag.IS_TEST
296 if is_verification
297 else _VerificationStatusFlag.IS_LIBRARY
298 )
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
314class RenderJob(ABC):
315 @property
316 @abstractmethod
317 def destination_name(self) -> pathlib.Path: ...
319 @abstractmethod
320 def write_to(self, fp: BinaryIO): ...
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 )
345 user_markdowns = UserMarkdowns.select_markdown(sources)
347 logger.info(" %s source files...", len(sources))
349 class SourceForDebug(BaseModel):
350 sources: SortedPathSet
351 markdowns: UserMarkdowns
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 )
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 )
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 )
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 )
405 if pj.display == DocumentOutputMode.never:
406 continue
408 page_jobs[pj.source_path] = pj
409 jobs.append(pj)
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 )
426 if md.front_matter.display == DocumentOutputMode.never:
427 continue
428 multis.append(job)
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
442@dataclass(frozen=True)
443class PlainRenderJob(RenderJob):
444 source_path: ForcePosixPath
445 content: bytes
447 @property
448 def destination_name(self):
449 return self.source_path
451 def write_to(self, fp: BinaryIO):
452 fp.write(self.content)
455class MarkdownRenderJob(RenderJob):
456 source_path: pathlib.Path
457 markdown: Markdown
459 @property
460 def destination_name(self):
461 return self.source_path
463 def write_to(self, fp: BinaryIO):
464 self.markdown.dump_merged(fp)
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
478 @property
479 def is_verification(self):
480 return self.stat.is_verification
482 @property
483 def display(self):
484 return self.front_matter.display or DocumentOutputMode.visible
486 def __str__(self) -> str:
487 return f"PageRenderJob(source_path={self.source_path!r},markdown={self.markdown!r},stat={self.stat!r})"
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 )
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 )
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"
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
532 return front_matter
534 @property
535 def destination_name(self):
536 return self.source_path.with_suffix(self.source_path.suffix + ".md")
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)
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 )
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)
576 code = read_text_normalized(self.source_path)
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 )
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 )
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
628 def __str__(self) -> str:
629 return f"MultiCodePageRenderJob(multi_documentation_of={self.markdown.multi_documentation_of!r})"
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
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()
648 @property
649 def is_verification(self):
650 return self.verification_status.is_test
652 @property
653 def display(self):
654 return self.markdown.front_matter.display or DocumentOutputMode.visible
656 @property
657 def destination_name(self) -> pathlib.Path:
658 return self.markdown.path
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 )
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)
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 ]
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 )
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 )
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 )
731@dataclass
732class IndexRenderJob(RenderJob):
733 page_jobs: dict[pathlib.Path, "PageRenderJob"]
734 multicode_docs: list[MultiCodePageRenderJob]
735 path_sort: PathSortOrder | None = None
737 index_md: Markdown | None = None
739 def __str__(self) -> str:
740 @dataclass
741 class _IndexRenderJob:
742 job_paths: Iterable[pathlib.Path]
744 s = repr(
745 _IndexRenderJob(
746 job_paths=self.page_jobs.keys(),
747 )
748 )
749 index = s.find("_IndexRenderJob")
750 return s[index + 1 :]
752 @property
753 def destination_name(self):
754 return pathlib.Path("index.md")
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)
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 )
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}/"
787 if category not in categories:
788 categories[category] = []
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)
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 )
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 )