Coverage for src / competitive_verifier / documents / builder.py: 98%
83 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 pathlib
2import shutil
3from logging import getLogger
4from urllib.parse import urlparse
6from pydantic import BaseModel
8import competitive_verifier_resources
9from competitive_verifier import git, github
10from competitive_verifier.models import VerificationInput, VerifyCommandResult
12from .config import ConfigYaml, load_config_yml
13from .front_matter import Markdown
14from .render import RenderJob
16logger = getLogger(__name__)
18_DOC_USAGE_SAMPLE_REPOSITORY = "NotLeonian/competitive-verifier"
20_MINIMAL_THEME = "jekyll-theme-minimal"
21_MINIMAL_REMOTE_THEME_SLUGS = frozenset(
22 {
23 "pages-themes/minimal",
24 }
25)
28def remote_theme_slug(remote_theme: str) -> str:
29 value = remote_theme.strip()
31 parsed = urlparse(value)
32 if parsed.scheme and parsed.netloc:
33 value = parsed.path
35 value = value.split("?", 1)[0]
36 value = value.split("#", 1)[0]
37 value = value.strip("/")
38 value = value.split("@", 1)[0]
40 value = value.removesuffix(".git")
42 return value.lower()
45def uses_minimal_theme(config_yml: ConfigYaml) -> bool:
46 if config_yml.remote_theme:
47 return remote_theme_slug(config_yml.remote_theme) in _MINIMAL_REMOTE_THEME_SLUGS
49 return config_yml.theme == _MINIMAL_THEME
52class DocumentBuilder(BaseModel):
53 verifications: VerificationInput
54 result: VerifyCommandResult
55 docs_dir: pathlib.Path
56 destination_dir: pathlib.Path
57 include: list[str] | None
58 exclude: list[str] | None
60 def build(self) -> bool:
61 logger.info("Working directory: %s", pathlib.Path.cwd().as_posix())
62 logger.info("Generate documents...")
64 # implementation
65 result = self.impl()
67 logger.info("Generated.")
68 logger.info(
69 competitive_verifier_resources.doc_usage(
70 markdown_dir_path=self.destination_dir,
71 repo_name=github.env.get_repository() or _DOC_USAGE_SAMPLE_REPOSITORY,
72 sample_repo_name=_DOC_USAGE_SAMPLE_REPOSITORY,
73 )
74 )
76 return result
78 def impl(self) -> bool:
79 self.destination_dir.mkdir(parents=True, exist_ok=True)
81 config_yml = load_config_yml(self.docs_dir)
82 logger.info("_config.yml: %s", config_yml)
84 index_md_path = self.docs_dir / "index.md"
85 index_md = Markdown.load_file(index_md_path) if index_md_path.exists() else None
87 # Write code documents.
88 self.write_code_docs(
89 config_yml=config_yml,
90 index_md=index_md,
91 static_dir=self.docs_dir / "static",
92 )
94 # Write _config.yml
95 (self.destination_dir / "_config.yml").write_bytes(config_yml.model_dump_yml())
97 # Copy static files
98 self.copy_static_files(
99 static_dir=self.docs_dir / "static",
100 config_yml=config_yml,
101 )
102 return True
104 def copy_static_files(
105 self,
106 *,
107 static_dir: pathlib.Path,
108 config_yml: ConfigYaml,
109 ):
110 logger.info("Copy library static files...")
111 for path, content in competitive_verifier_resources.jekyll_files().items():
112 file_dst = self.destination_dir / path
113 logger.debug("Writing to %s", file_dst.as_posix())
114 file_dst.parent.mkdir(parents=True, exist_ok=True)
115 file_dst.write_bytes(content)
117 if uses_minimal_theme(config_yml):
118 logger.info("Copy jekyll-theme-minimal overrides...")
119 for (
120 path,
121 content,
122 ) in competitive_verifier_resources.jekyll_theme_override_files(
123 "jekyll-theme-minimal",
124 ).items():
125 file_dst = self.destination_dir / path
126 logger.debug("Writing to %s", file_dst.as_posix())
127 file_dst.parent.mkdir(parents=True, exist_ok=True)
128 file_dst.write_bytes(content)
130 logger.info("Copy user static files...")
131 try:
132 if static_dir.is_dir():
133 shutil.copytree(
134 static_dir,
135 self.destination_dir,
136 dirs_exist_ok=True,
137 )
138 except Exception:
139 logger.exception("Failed to copy user static files.")
141 def write_code_docs(
142 self,
143 *,
144 config_yml: ConfigYaml,
145 index_md: Markdown | None,
146 static_dir: pathlib.Path | None,
147 ):
148 logger.info("Write document files...")
150 exclude = (self.exclude or []) + (config_yml.exclude or [])
151 if static_dir and static_dir.is_relative_to("."):
152 exclude.append(self.docs_dir.relative_to(".").as_posix())
154 sources = git.ls_files(*(self.include or []))
155 if exclude:
156 sources -= git.ls_files(*exclude)
158 for job in RenderJob.enumerate_jobs(
159 sources=sources,
160 verifications=self.verifications,
161 result=self.result,
162 config=config_yml,
163 index_md=index_md,
164 ):
165 logger.debug(job)
166 dst = self.destination_dir / job.destination_name
167 dst.parent.mkdir(parents=True, exist_ok=True)
168 logger.info("writing to %s", dst.as_posix())
169 with dst.open("wb") as fp:
170 job.write_to(fp)