|
| 1 | +"""Generate options descriptions from the FracturedJson wiki.""" # noqa: INP001 |
| 2 | + |
| 3 | +import re |
| 4 | + |
| 5 | +import requests |
| 6 | +from bs4 import BeautifulSoup |
| 7 | + |
| 8 | +WIKI_OPTIONS = "https://github.com/j-brooke/FracturedJson/wiki/Options" |
| 9 | + |
| 10 | + |
| 11 | +def to_snake_case(name: str, upper: bool = False) -> str: |
| 12 | + """Convert PascalCase or camelCase to SNAKE_CASE or snake_case.""" |
| 13 | + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) |
| 14 | + s2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1) |
| 15 | + return s2.upper() if upper else s2.lower() |
| 16 | + |
| 17 | + |
| 18 | +def fetch_options() -> dict[str, str]: |
| 19 | + # Fetch the page |
| 20 | + resp = requests.get(WIKI_OPTIONS) # noqa: S113 |
| 21 | + resp.raise_for_status() |
| 22 | + |
| 23 | + # Parse HTML |
| 24 | + soup = BeautifulSoup(resp.text, "html.parser") |
| 25 | + |
| 26 | + options: dict[str, str] = {} |
| 27 | + option_name_map: dict[str, str] = {} |
| 28 | + started = False |
| 29 | + |
| 30 | + # Each option name is in an H2 inside a DIV.markdown-heading |
| 31 | + for heading_div in soup.select("div.markdown-heading > h2"): |
| 32 | + option_name = heading_div.get_text(strip=True) |
| 33 | + |
| 34 | + # Some H2 tags are misinterpreted before this first option |
| 35 | + if not started and option_name != "MaxTotalLineLength": |
| 36 | + continue |
| 37 | + started = True |
| 38 | + |
| 39 | + # Walk forward to find the first <p> after this heading |
| 40 | + desc = None |
| 41 | + for sib in heading_div.parent.next_siblings: |
| 42 | + # Skip non-tag items (whitespace, etc.) |
| 43 | + if not getattr(sib, "name", None): |
| 44 | + continue |
| 45 | + # Stop if we hit another heading section |
| 46 | + if sib.name in {"h1", "h2", "h3"} or ( |
| 47 | + sib.name == "div" and "markdown-heading" in sib.get("class", []) |
| 48 | + ): |
| 49 | + break |
| 50 | + # First paragraph is the description |
| 51 | + p = sib.find("p") if sib.name != "p" else sib |
| 52 | + if p: |
| 53 | + # Preserve spaces around <code> tags |
| 54 | + desc = p.get_text(" ", strip=True) |
| 55 | + break |
| 56 | + |
| 57 | + if desc: |
| 58 | + desc = re.sub(r"\s+\(.*", "", desc) |
| 59 | + desc = re.sub(r"\s+", " ", desc) |
| 60 | + desc = re.sub(r"The default.*", "", desc) |
| 61 | + py_option_name = to_snake_case(option_name, upper=False) |
| 62 | + option_name_map[py_option_name] = option_name |
| 63 | + options[py_option_name] = desc |
| 64 | + |
| 65 | + # Descriptions may refer to .NET names; convert to Pythonic names |
| 66 | + for py_name, desc in options.items(): |
| 67 | + for py_name_2, dotnet_name in option_name_map.items(): |
| 68 | + desc = desc.replace(dotnet_name, py_name_2) # noqa: PLW2901 |
| 69 | + desc = desc.replace("_", "-") # noqa: PLW2901 |
| 70 | + options[py_name] = desc |
| 71 | + return options |
| 72 | + |
| 73 | + |
| 74 | +if __name__ == "__main__": |
| 75 | + options = fetch_options() |
| 76 | + with open("src/fractured_json/generated/option_descriptions.py", "w") as f: |
| 77 | + f.write("# Auto-generated file; do not edit.\n") |
| 78 | + f.write("FLAG_DESCRIPTIONS = {\n") |
| 79 | + f.writelines(f' "{name}": "{desc}",\n' for name, desc in sorted(options.items())) |
| 80 | + f.write("}\n") |
0 commit comments