build_docs.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. # Ultralytics YOLO 🚀, AGPL-3.0 license
  2. """
  3. This Python script is designed to automate the building and post-processing of MkDocs documentation, particularly for
  4. projects with multilingual content. It streamlines the workflow for generating localized versions of the documentation
  5. and updating HTML links to ensure they are correctly formatted.
  6. Key Features:
  7. - Automated building of MkDocs documentation: The script compiles both the main documentation and
  8. any localized versions specified in separate MkDocs configuration files.
  9. - Post-processing of generated HTML files: After the documentation is built, the script updates all
  10. HTML files to remove the '.md' extension from internal links. This ensures that links in the built
  11. HTML documentation correctly point to other HTML pages rather than Markdown files, which is crucial
  12. for proper navigation within the web-based documentation.
  13. Usage:
  14. - Run the script from the root directory of your MkDocs project.
  15. - Ensure that MkDocs is installed and that all MkDocs configuration files (main and localized versions)
  16. are present in the project directory.
  17. - The script first builds the documentation using MkDocs, then scans the generated HTML files in the 'site'
  18. directory to update the internal links.
  19. - It's ideal for projects where the documentation is written in Markdown and needs to be served as a static website.
  20. Note:
  21. - This script is built to be run in an environment where Python and MkDocs are installed and properly configured.
  22. """
  23. import os
  24. import re
  25. import shutil
  26. import subprocess
  27. from pathlib import Path
  28. from tqdm import tqdm
  29. os.environ["JUPYTER_PLATFORM_DIRS"] = "1" # fix DeprecationWarning: Jupyter is migrating to use standard platformdirs
  30. DOCS = Path(__file__).parent.resolve()
  31. SITE = DOCS.parent / "site"
  32. def prepare_docs_markdown(clone_repos=True):
  33. """Build docs using mkdocs."""
  34. if SITE.exists():
  35. print(f"Removing existing {SITE}")
  36. shutil.rmtree(SITE)
  37. # Get hub-sdk repo
  38. if clone_repos:
  39. repo = "https://github.com/ultralytics/hub-sdk"
  40. local_dir = DOCS.parent / Path(repo).name
  41. if not local_dir.exists():
  42. os.system(f"git clone {repo} {local_dir}")
  43. os.system(f"git -C {local_dir} pull") # update repo
  44. shutil.rmtree(DOCS / "en/hub/sdk", ignore_errors=True) # delete if exists
  45. shutil.copytree(local_dir / "docs", DOCS / "en/hub/sdk") # for docs
  46. shutil.rmtree(DOCS.parent / "hub_sdk", ignore_errors=True) # delete if exists
  47. shutil.copytree(local_dir / "hub_sdk", DOCS.parent / "hub_sdk") # for mkdocstrings
  48. print(f"Cloned/Updated {repo} in {local_dir}")
  49. # Add frontmatter
  50. for file in tqdm((DOCS / "en").rglob("*.md"), desc="Adding frontmatter"):
  51. update_markdown_files(file)
  52. def update_page_title(file_path: Path, new_title: str):
  53. """Update the title of an HTML file."""
  54. # Read the content of the file
  55. with open(file_path, encoding="utf-8") as file:
  56. content = file.read()
  57. # Replace the existing title with the new title
  58. updated_content = re.sub(r"<title>.*?</title>", f"<title>{new_title}</title>", content)
  59. # Write the updated content back to the file
  60. with open(file_path, "w", encoding="utf-8") as file:
  61. file.write(updated_content)
  62. def update_html_head(script=""):
  63. """Update the HTML head section of each file."""
  64. html_files = Path(SITE).rglob("*.html")
  65. for html_file in tqdm(html_files, desc="Processing HTML files"):
  66. with html_file.open("r", encoding="utf-8") as file:
  67. html_content = file.read()
  68. if script in html_content: # script already in HTML file
  69. return
  70. head_end_index = html_content.lower().rfind("</head>")
  71. if head_end_index != -1:
  72. # Add the specified JavaScript to the HTML file just before the end of the head tag.
  73. new_html_content = html_content[:head_end_index] + script + html_content[head_end_index:]
  74. with html_file.open("w", encoding="utf-8") as file:
  75. file.write(new_html_content)
  76. def update_subdir_edit_links(subdir="", docs_url=""):
  77. """Update the HTML head section of each file."""
  78. from bs4 import BeautifulSoup
  79. if str(subdir[0]) == "/":
  80. subdir = str(subdir[0])[1:]
  81. html_files = (SITE / subdir).rglob("*.html")
  82. for html_file in tqdm(html_files, desc="Processing subdir files"):
  83. with html_file.open("r", encoding="utf-8") as file:
  84. soup = BeautifulSoup(file, "html.parser")
  85. # Find the anchor tag and update its href attribute
  86. a_tag = soup.find("a", {"class": "md-content__button md-icon"})
  87. if a_tag and a_tag["title"] == "Edit this page":
  88. a_tag["href"] = f"{docs_url}{a_tag['href'].split(subdir)[-1]}"
  89. # Write the updated HTML back to the file
  90. with open(html_file, "w", encoding="utf-8") as file:
  91. file.write(str(soup))
  92. def update_markdown_files(md_filepath: Path):
  93. """Creates or updates a Markdown file, ensuring frontmatter is present."""
  94. if md_filepath.exists():
  95. content = md_filepath.read_text().strip()
  96. # Replace apostrophes
  97. content = content.replace("‘", "'").replace("’", "'")
  98. # Add frontmatter if missing
  99. if not content.strip().startswith("---\n"):
  100. header = "---\ncomments: true\ndescription: TODO ADD DESCRIPTION\nkeywords: TODO ADD KEYWORDS\n---\n\n"
  101. content = header + content
  102. # Ensure MkDocs admonitions "=== " lines are preceded and followed by empty newlines
  103. lines = content.split("\n")
  104. new_lines = []
  105. for i, line in enumerate(lines):
  106. stripped_line = line.strip()
  107. if stripped_line.startswith("=== "):
  108. if i > 0 and new_lines[-1] != "":
  109. new_lines.append("")
  110. new_lines.append(line)
  111. if i < len(lines) - 1 and lines[i + 1].strip() != "":
  112. new_lines.append("")
  113. else:
  114. new_lines.append(line)
  115. content = "\n".join(new_lines)
  116. # Add EOF newline if missing
  117. if not content.endswith("\n"):
  118. content += "\n"
  119. # Save page
  120. md_filepath.write_text(content)
  121. return
  122. def update_docs_html():
  123. """Updates titles, edit links and head sections of HTML documentation for improved accessibility and relevance."""
  124. update_page_title(SITE / "404.html", new_title="Ultralytics Docs - Not Found")
  125. # Update edit links
  126. update_subdir_edit_links(
  127. subdir="hub/sdk/", # do not use leading slash
  128. docs_url="https://github.com/ultralytics/hub-sdk/tree/main/docs/",
  129. )
  130. # Update HTML file head section
  131. script = ""
  132. if any(script):
  133. update_html_head(script)
  134. def main():
  135. """Builds docs, updates titles and edit links, and prints local server command."""
  136. prepare_docs_markdown()
  137. # Build the main documentation
  138. print(f"Building docs from {DOCS}")
  139. subprocess.run(f"mkdocs build -f {DOCS.parent}/mkdocs.yml --strict", check=True, shell=True)
  140. print(f"Site built at {SITE}")
  141. # Update docs HTML pages
  142. update_docs_html()
  143. # Show command to serve built website
  144. print('Docs built correctly ✅\nServe site at http://localhost:8000 with "python -m http.server --directory site"')
  145. if __name__ == "__main__":
  146. main()