|
| 1 | +""" |
| 2 | +Script to covert wiki links to be used in Github Pages Docs. GitHub Wiki uses a Wiki-style link while GitHub Pages uses standard Markdown links. |
| 3 | +""" |
| 4 | + |
| 5 | +import os |
| 6 | +import re |
| 7 | +import sys |
| 8 | +from pathlib import Path |
| 9 | +from typing import Dict, List |
| 10 | + |
| 11 | +# Configuration |
| 12 | +base_url = sys.argv[1].rstrip("/") |
| 13 | +wiki_subfolder = sys.argv[2].strip("/") |
| 14 | +github_pages_base = base_url + "/" |
| 15 | +wiki_link_base = r"https://github.com/DUNE-DAQ/drunc/wiki/" |
| 16 | + |
| 17 | +GH_pages_folder_name = "developer-documentation" |
| 18 | + |
| 19 | + |
| 20 | +def parse_markdown_nav(file_path: Path) -> List[tuple[list[str], str]]: |
| 21 | + """ |
| 22 | + Convert Wiki sidebar navigation into (stack, url). |
| 23 | +
|
| 24 | + Args: |
| 25 | + file_path: The path to the _Sidebar.md file to parse. |
| 26 | + Returns: |
| 27 | + A list of tuples containing (stack, url) representing the navigation structure. |
| 28 | + """ |
| 29 | + stack, entries = [], [] |
| 30 | + with open(file_path) as f: |
| 31 | + for line in f: |
| 32 | + indent = len(line) - len(line.lstrip(" ")) |
| 33 | + # Check if line is a list item |
| 34 | + match = re.match(r"\s*[-*]\s*(.+)", line) |
| 35 | + |
| 36 | + if not match: |
| 37 | + continue |
| 38 | + |
| 39 | + content = match.group(1).strip() |
| 40 | + level = indent // 2 |
| 41 | + |
| 42 | + stack = stack[:level] # rewind stack to current level |
| 43 | + link_match = re.match(r"\[([^\]]+)\]\(([^)]+)\)", content) |
| 44 | + |
| 45 | + if link_match: |
| 46 | + title, url = link_match.groups() |
| 47 | + stack.append(title) |
| 48 | + entries.append((tuple(stack), url)) |
| 49 | + stack.pop() # remove the curent file path, go back to root folder |
| 50 | + else: |
| 51 | + # section header without link |
| 52 | + stack.append(content) |
| 53 | + |
| 54 | + return entries |
| 55 | + |
| 56 | + |
| 57 | +def process_wiki_content( |
| 58 | + filename: str, folder_base: str, index_dict: Dict[str, str] |
| 59 | +) -> None: |
| 60 | + """ |
| 61 | + Process a wiki file and write converted output in GH Pages format. |
| 62 | +
|
| 63 | + Args: |
| 64 | + filename: The name of the wiki file to process (an .md file). |
| 65 | + folder_base: The folder name under developer-documentation where the file will be written. |
| 66 | + index_dict: A mapping of page names to their new GH Pages location. |
| 67 | + """ |
| 68 | + |
| 69 | + with open(filename, "r", encoding="utf-8") as f: |
| 70 | + content = f.read() |
| 71 | + |
| 72 | + # Replace GitHub Wiki links with GitHub Pages links |
| 73 | + # Wiki links point to other wiki pages but we need to point to the GitHub Pages site |
| 74 | + content = re.sub(wiki_link_base, github_pages_base, content) |
| 75 | + |
| 76 | + # Update links based on index dictionary containing page names and the new page location |
| 77 | + for page_name, new_page_location in index_dict.items(): |
| 78 | + content = re.sub(f"/{page_name}", f"/{new_page_location}", content) |
| 79 | + |
| 80 | + # Convert [[Link Name]] to [Link Name](Link-Name.html) |
| 81 | + content = re.sub( |
| 82 | + r"\[ \[([^\|\]]+)\]\]", |
| 83 | + lambda m: f"[{m.group(1)}]({m.group(1).replace(' ', '-')}.html)", |
| 84 | + content, |
| 85 | + ) |
| 86 | + # Convert [[Link Text|Page Name]] to [Link Text](Page-Name.html) |
| 87 | + content = re.sub( |
| 88 | + r"\[\[([^\|\]]+)\|([^\|\]]+)\]\]", |
| 89 | + lambda m: f"[{m.group(1)}]({m.group(2).replace(' ', '-')}.html)", |
| 90 | + content, |
| 91 | + ) |
| 92 | + # Ensure code block fences have blank lines before and after |
| 93 | + content = re.sub( |
| 94 | + r"([^\n])(```.*?```)([^\n])", |
| 95 | + r"\1\n\2\n\3", |
| 96 | + content, |
| 97 | + flags=re.DOTALL, |
| 98 | + ) |
| 99 | + content = re.sub(r"([^\n])(```)", r"\1\n\2", content) |
| 100 | + content = re.sub(r"(```)([^\n])", r"\1\n\2", content) |
| 101 | + |
| 102 | + # Remove checbokex in lists |
| 103 | + content = re.sub(r"- \[(?: |x|X)\]", "- ", content) |
| 104 | + |
| 105 | + # Fix leading spaces before list markers |
| 106 | + content = re.sub(r"^\s+-", "-", content, flags=re.MULTILINE) |
| 107 | + |
| 108 | + # Make sure a blank line exists before list items |
| 109 | + content = re.sub(r"([^\n])\n(?=\s*[-*+]\s)", r"\1\n\n", content) |
| 110 | + |
| 111 | + # The folder where the new files will be written and will be the base for the GH Pages wiki docs |
| 112 | + new_wiki_dir = Path("..") / GH_pages_folder_name |
| 113 | + |
| 114 | + # Make sure the folder structure exists |
| 115 | + os.makedirs(new_wiki_dir / folder_base, exist_ok=True) |
| 116 | + |
| 117 | + # Write to index file a list of pages incuded in the folder |
| 118 | + with open( |
| 119 | + os.path.join(new_wiki_dir / folder_base, "index.md"), "a", encoding="utf-8" |
| 120 | + ) as f: |
| 121 | + print( |
| 122 | + f"* [{filename.removesuffix('.md').replace('-', ' ')}]({filename}) \n", |
| 123 | + file=f, |
| 124 | + ) |
| 125 | + |
| 126 | + # Write converted content to new file in the developer-documentation folder to be displayed on GH Pages |
| 127 | + new_file_name = os.path.join(new_wiki_dir / folder_base, filename) |
| 128 | + |
| 129 | + with open(new_file_name, "a", encoding="utf-8") as f: |
| 130 | + f.write(content) |
| 131 | + |
| 132 | + |
| 133 | +def clean_filename(filename: str) -> str | None: |
| 134 | + """ |
| 135 | + Check if a markdown file exists, rename it |
| 136 | + to replace spaces with hyphens, and return the base name (without .md). |
| 137 | +
|
| 138 | + Args: |
| 139 | + filename: The original filename to check and rename (with .md extension). |
| 140 | + """ |
| 141 | + if ( |
| 142 | + not os.path.exists(filename) |
| 143 | + or not filename.endswith(".md") |
| 144 | + or filename == "index.md" |
| 145 | + ): |
| 146 | + return None |
| 147 | + |
| 148 | + # Rename file to replace spaces with hyphens if necessary |
| 149 | + new_name = filename.replace(" ", "-") |
| 150 | + if filename != new_name: |
| 151 | + os.rename(filename, new_name) |
| 152 | + |
| 153 | + # Return filename base (without .md) |
| 154 | + return new_name.removesuffix(".md") |
| 155 | + |
| 156 | + |
| 157 | +def generate_index_dict(sidebar_nav: list[tuple[list[str], str]]) -> dict[str, str]: |
| 158 | + """ |
| 159 | + Create the index dictionary mapping filename bases |
| 160 | + to their final destination paths. Renames files as a side effect. |
| 161 | +
|
| 162 | + Args: |
| 163 | + sidebar_nav: The parsed sidebar navigation entries as a list of tuples. |
| 164 | + Returns: |
| 165 | + index_dict: A dictionary mapping filenames to their new paths to be displayed in GH Pages. |
| 166 | + """ |
| 167 | + index_dict = {} |
| 168 | + |
| 169 | + for entry in sidebar_nav: |
| 170 | + # Only process entries with group and page (len == 2) |
| 171 | + if len(entry[0]) != 2: |
| 172 | + continue |
| 173 | + |
| 174 | + group, _ = entry[0] |
| 175 | + link = entry[1] |
| 176 | + |
| 177 | + folder_base = group |
| 178 | + filename = link.split("/")[-1] + ".md" |
| 179 | + |
| 180 | + standard_base_name = clean_filename(filename) |
| 181 | + |
| 182 | + if standard_base_name: |
| 183 | + # Construct the final path using the standardised base name |
| 184 | + new_file_name = os.path.join( |
| 185 | + GH_pages_folder_name, folder_base, standard_base_name |
| 186 | + ) |
| 187 | + index_dict[standard_base_name] = new_file_name |
| 188 | + |
| 189 | + return index_dict |
| 190 | + |
| 191 | + |
| 192 | +def generate_gh_pages( |
| 193 | + sidebar_nav: list[tuple[list[str], str]], index_dict: dict[str, str] |
| 194 | +) -> None: |
| 195 | + """ |
| 196 | + Generate the files GH Pages markdown files using the generated index dictionary. |
| 197 | +
|
| 198 | + Args: |
| 199 | + sidebar_nav: The parsed sidebar navigation entries as a list of tuples. |
| 200 | + index_dict: A dictionary mapping filenames to their new paths to be displayed in GH Pages. |
| 201 | + """ |
| 202 | + for entry in sidebar_nav: |
| 203 | + if len(entry[0]) != 2: |
| 204 | + continue |
| 205 | + |
| 206 | + group, _ = entry[0] |
| 207 | + link = entry[1] |
| 208 | + |
| 209 | + folder_base = group |
| 210 | + filename = link.split("/")[-1] + ".md" |
| 211 | + standard_base_name = clean_filename(filename) |
| 212 | + |
| 213 | + if standard_base_name: |
| 214 | + current_filename = standard_base_name + ".md" |
| 215 | + process_wiki_content(current_filename, folder_base, index_dict) |
| 216 | + |
| 217 | + |
| 218 | +# Main script execution |
| 219 | +wiki_dir = os.path.join("docs", wiki_subfolder) |
| 220 | +os.chdir(wiki_dir) |
| 221 | +sidebar_nav = parse_markdown_nav("_Sidebar.md") |
| 222 | + |
| 223 | +# Generate index of page names and their GH pages location |
| 224 | +index_dict = generate_index_dict(sidebar_nav) |
| 225 | + |
| 226 | +# Process files and generate GH pages |
| 227 | +generate_gh_pages(sidebar_nav, index_dict) |
| 228 | + |
| 229 | +# Write top-level index file for Developer Documentation |
| 230 | +gh_wiki_dir = Path("..") / GH_pages_folder_name |
| 231 | +index_path = os.path.join(gh_wiki_dir, "index.md") |
| 232 | + |
| 233 | +with open(index_path, "w", encoding="utf-8") as f: |
| 234 | + f.write("# Developer documentation \n\n") |
| 235 | + # Add subfolders and their files as nested lists |
| 236 | + for folder_base, subdirs, files in os.walk(gh_wiki_dir): |
| 237 | + for subdir in sorted(subdirs): |
| 238 | + f.write(f"* [{subdir}]({subdir}/index.md)\n") |
| 239 | + |
| 240 | + sub_path = os.path.join(folder_base, subdir) |
| 241 | + for subfile in os.listdir(sub_path): |
| 242 | + if subfile != "index.md" and subfile.endswith(".md"): |
| 243 | + display_name = subfile.removesuffix(".md").replace("-", " ") |
| 244 | + f.write(f" - [{display_name}]({subdir}/{subfile})\n") |
0 commit comments