prebuild.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/env python3
  2. """Updates files in preparation for a jekyll build.
  3. Used from .github/workflows/gh_pages.yaml. This updates the file and directory
  4. structure prior to the jekyll build.
  5. """
  6. import dataclasses
  7. import os
  8. from pathlib import Path
  9. import re
  10. from typing import Optional
  11. __copyright__ = """
  12. Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  13. Exceptions. See /LICENSE for license information.
  14. SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  15. """
  16. @dataclasses.dataclass
  17. class ChildDir:
  18. """Tracks whether a child directory has grandchildren."""
  19. title: str
  20. has_grandchildren: bool = False
  21. def get_title(f: Path, content: str) -> str:
  22. """Returns a file's title according to markdown.
  23. Replacements are for YAML compatibility in `add_frontmatter`.
  24. """
  25. m = re.search("^# (.*)$", content, re.MULTILINE)
  26. assert m, f
  27. title = m[1]
  28. title = title.replace("\\", "\\\\")
  29. title = title.replace('"', '\\"')
  30. title = re.sub("`([^`]+)`", r"<code>\1</code>", title)
  31. return title
  32. def add_frontmatter(
  33. f: Path,
  34. orig_content: str,
  35. titles: list[str],
  36. nav_order: Optional[int],
  37. has_children: bool,
  38. ) -> None:
  39. """Adds frontmatter to a file."""
  40. content = "---\n"
  41. assert len(titles) <= 3
  42. content += f'title: "{titles[-1]}"\n'
  43. if len(titles) >= 2:
  44. content += f'parent: "{titles[-2]}"\n'
  45. if len(titles) == 3:
  46. content += f'grand_parent: "{titles[-3]}"\n'
  47. if nav_order is not None:
  48. content += f"nav_order: {nav_order}\n"
  49. if has_children:
  50. # has_toc is disabling the list of child files in a separate table of
  51. # contents.
  52. content += "has_children: true\nhas_toc: false\n"
  53. content += "---\n\n" + orig_content
  54. f.write_text(content)
  55. def label_subdir(
  56. subdir_str: str,
  57. top_nav_order: int,
  58. parent_title: Optional[str] = None,
  59. grandchild_dirs: bool = False,
  60. ) -> None:
  61. """Automatically adds child information to a subdirectory's markdown files.
  62. This is in support of navigation.
  63. """
  64. assert not (parent_title and grandchild_dirs)
  65. subdir = Path(subdir_str)
  66. readme = subdir / "README.md"
  67. readme_content = readme.read_text()
  68. def include_child(md_file: Path) -> bool:
  69. """Returns whether md_file is a child of this label."""
  70. # The top-level README.md isn't a child.
  71. if md_file == readme:
  72. return False
  73. # Skip directories that aren't part of the repository.
  74. if "/node_modules/" in str(md_file):
  75. return False
  76. if "/dist/" in str(md_file):
  77. return False
  78. return True
  79. readme_title = get_title(readme, readme_content)
  80. readme_titles = [readme_title]
  81. if parent_title:
  82. readme_titles.insert(0, parent_title)
  83. children = [x for x in subdir.glob("**/*.md") if include_child(x)]
  84. add_frontmatter(
  85. readme, readme_content, readme_titles, top_nav_order, bool(children)
  86. )
  87. if grandchild_dirs:
  88. # When adding grandchildren, we cluster files by child directory.
  89. child_dirs = {}
  90. for child in children:
  91. if child.name == "README.md":
  92. title = get_title(child, child.read_text())
  93. child_dirs[child.parent] = ChildDir(title)
  94. for child in children:
  95. if child.name != "README.md" and child.parent in child_dirs:
  96. child_dirs[child.parent].has_grandchildren = True
  97. for child in children:
  98. child_content = child.read_text()
  99. child_title = get_title(child, child_content)
  100. child_nav_order = None
  101. if subdir_str == "proposals":
  102. # Use proposal numbers as part of the title and ordering.
  103. m = re.match(r"p(\d+).md", child.name)
  104. # Skip files that aren't proposals.
  105. if not m:
  106. continue
  107. child_title = f"#{m[1]}: {child_title}"
  108. child_nav_order = int(m[1])
  109. titles = [readme_title, child_title]
  110. has_children = False
  111. if parent_title:
  112. titles.insert(0, parent_title)
  113. elif grandchild_dirs and child.parent in child_dirs:
  114. if child.name == "README.md":
  115. has_children = child_dirs[child.parent].has_grandchildren
  116. else:
  117. dir_title = child_dirs[child.parent].title
  118. titles.insert(1, dir_title)
  119. add_frontmatter(
  120. child, child_content, titles, child_nav_order, has_children
  121. )
  122. def label_root_file(
  123. name: str, title: str, top_nav_order: int, has_children: bool = False
  124. ) -> None:
  125. """Adds frontmatter to a root file, like CONTRIBUTING.md."""
  126. f = Path(name)
  127. add_frontmatter(f, f.read_text(), [title], top_nav_order, has_children)
  128. def main() -> None:
  129. # Ensure this runs from the repo root.
  130. os.chdir(Path(__file__).parents[1])
  131. # The external symlink is created by scripts/create_compdb.py, and can
  132. # interfere with local execution.
  133. external = Path("external")
  134. if external.exists():
  135. external.unlink()
  136. # Move files to the repo root.
  137. for f in Path("website").iterdir():
  138. if f.name in ["README.md", ".jekyll-cache", "_site"]:
  139. continue
  140. f.rename(f.name)
  141. # Use an object for a reference.
  142. nav_order = [0]
  143. # Returns an incrementing value for ordering.
  144. def next(nav_order: list[int]) -> int:
  145. nav_order[0] += 1
  146. return nav_order[0]
  147. label_root_file("README.md", "Home", next(nav_order))
  148. label_root_file("CONTRIBUTING.md", "Contributing", next(nav_order))
  149. label_subdir("docs/design", next(nav_order), grandchild_dirs=True)
  150. label_subdir("docs/guides", next(nav_order))
  151. label_subdir("docs/project", next(nav_order), grandchild_dirs=True)
  152. label_subdir("docs/spec", next(nav_order))
  153. # Provide a small file to cluster implementation-related directories.
  154. label_root_file(
  155. "implementation.md",
  156. "Implementation",
  157. next(nav_order),
  158. has_children=True,
  159. )
  160. label_subdir("utils", next(nav_order))
  161. label_subdir("proposals", next(nav_order))
  162. label_root_file("CODE_OF_CONDUCT.md", "Code of conduct", next(nav_order))
  163. label_root_file("SECURITY.md", "Security policy", next(nav_order))
  164. # Reset the order for the implementation children.
  165. nav_order[0] = 0
  166. label_subdir(
  167. "toolchain/docs", next(nav_order), parent_title="Implementation"
  168. )
  169. label_subdir("explorer", next(nav_order), parent_title="Implementation")
  170. label_subdir("testing", next(nav_order), parent_title="Implementation")
  171. if __name__ == "__main__":
  172. main()