From 7de4b299b7c188af458df9c18b1429f6a66b5bf8 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Thu, 4 Dec 2025 12:07:06 +0800 Subject: [PATCH] refactor: Update docs generator --- generator/docs_generator/generator.py | 175 ++++++++++++++++++-------- 1 file changed, 125 insertions(+), 50 deletions(-) diff --git a/generator/docs_generator/generator.py b/generator/docs_generator/generator.py index b41288b12..47f76301b 100644 --- a/generator/docs_generator/generator.py +++ b/generator/docs_generator/generator.py @@ -2,11 +2,36 @@ import re import yaml + SOURCE_DIR = "../../api" DEST_DIR = "../../docs" + os.makedirs(DEST_DIR, exist_ok=True) + +def escape_generics(text): + """Escape < and > in generic types for MDX compatibility.""" + if not text: + return text + return text.replace('<', '<').replace('>', '>') + + +def fix_html_for_jsx(text): + """Fix HTML attributes for JSX compatibility.""" + if not text: + return text + # Replace class= with className= + return text.replace('class=', 'className=') + + +def escape_generics_in_link_text(text): + """Escape generics in markdown link text only.""" + if not text: + return text + return text.replace('<', '\\<').replace('>', '\\>') + + def get_namespace(yaml_data): """Extract namespace from the YAML data.""" for item in yaml_data.get('body', []): @@ -18,38 +43,46 @@ def get_namespace(yaml_data): return fact.get('value', '') return '' + def extract_metadata(yaml_data, is_index=False): """Extract front-matter metadata from DocFX ApiPage YAML.""" title = yaml_data.get('title', '') if not title: return {} + # First remove generics and brackets from the full title + clean_title = re.sub(r'<[^>]+>', '', title) + clean_title = re.sub(r'\[[^\]]+\]', '', clean_title) + if is_index: - # Only keep the last part after the last dot - clean_title = title.split(".")[-1] + clean_title = clean_title.split(".")[-1] else: - # Default behavior: last word - words = title.split() + words = clean_title.split() clean_title = words[-1] if words else '' - clean_title = re.sub(r'<[^>]+>', '', clean_title) - clean_title = re.sub(r'\[[^\]]+\]', '', clean_title) - return {'title': clean_title} + def convert_to_path(s): + """Convert DocFX URL to site path.""" if s.lower().endswith(".html"): - s = s[10:] - s = s[:-5] - # Merge Core and Shared into the same path - s = s.replace("SwiftlyS2.Core.", "SwiftlyS2.").replace("SwiftlyS2.Shared.", "SwiftlyS2.") + s = s[:-5] # Remove .html first + + # Remove prefixes in order of specificity + for prefix in ["SwiftlyS2.Core.", "SwiftlyS2.Shared.", "SwiftlyS2."]: + if s.startswith(prefix): + s = s[len(prefix):] + break + path = "/".join(s.split(".")).lower() + # Transform -NUMBER to t repeated NUMBER times parts = path.split("/") parts[-1] = transform_filename(parts[-1]) path = "/".join(parts) return path + def transform_filename(base_name): """ If filename ends with -NUMBER, replace with NUMBER times 't'. @@ -62,8 +95,34 @@ def transform_filename(base_name): return name + ("t" * num) return base_name + +def format_type_link(type_info): + """Format type information as ApiParam component.""" + if isinstance(type_info, list): + parts = [] + for t in type_info: + if isinstance(t, dict): + text = t.get('text', '') + url = t.get('url', '') + if url.endswith('.html'): + url = "/docs/api/" + convert_to_path(url) + parts.append((text, url)) + else: + parts.append((str(t), '')) + type_text = ''.join([p[0] for p in parts]) + type_href = next((p[1] for p in parts if p[1]), '') + return type_text, type_href + elif isinstance(type_info, dict): + text = type_info.get('text', '') + url = type_info.get('url', '') + if url.endswith('.html'): + url = "/docs/api/" + convert_to_path(url) + return text, url + return str(type_info), '' + + def generate_markdown(yaml_data): - """Generate Markdown content from DocFX ApiPage YAML.""" + """Generate MDX content from DocFX ApiPage YAML.""" md = "" namespace = get_namespace(yaml_data) @@ -74,7 +133,8 @@ def generate_markdown(yaml_data): md += f"# {api1_title}\n\n" if 'src' in item: src = item['src'].replace('/blob/main', '/blob/master') - md += f"[View Source]({src})\n\n" + md += f'\n\n' + if 'facts' in item: for fact in item['facts']: fact_name = fact.get('name', '') @@ -84,19 +144,28 @@ def generate_markdown(yaml_data): fact_url = fact_value.get('url', '') if fact_url and fact_url.endswith('.html'): fact_url = "/docs/api/" + convert_to_path(fact_url) - md += f"**{fact_name}**: [{fact_text}]({fact_url})\n\n" + md += f"**{fact_name}**: [{escape_generics_in_link_text(fact_text)}]({fact_url})\n\n" else: md += f"**{fact_name}**: {fact_text}\n\n" else: md += f"**{fact_name}**: {fact_value}\n\n" + if 'markdown' in item: md += f"{item['markdown']}\n\n" + if 'h2' in item: md += f"## {item['h2']}\n\n" + if 'h4' in item: - md += f"#### {item['h4']}\n\n" + h4_text = item['h4'] + if h4_text in ['Parameters', 'Returns', 'Field Value', 'Property Value']: + md += f"{h4_text}\n\n" + else: + md += f"#### {h4_text}\n\n" + if 'code' in item: md += "```csharp\n" + item['code'] + "\n```\n\n" + if 'inheritance' in item: for inherit in item['inheritance']: inherit_text = inherit.get('text', '') @@ -104,10 +173,11 @@ def generate_markdown(yaml_data): if inherit_url: if inherit_url.endswith('.html'): inherit_url = "/docs/api/" + convert_to_path(inherit_url) - md += f"- [{inherit_text}]({inherit_url})\n" + md += f"- [{escape_generics_in_link_text(inherit_text)}]({inherit_url})\n" else: md += f"- {inherit_text}\n" md += "\n" + if 'list' in item: for list_item in item['list']: list_text = list_item.get('text', '') @@ -115,42 +185,41 @@ def generate_markdown(yaml_data): if list_url: if list_url.endswith('.html'): list_url = "/docs/api/" + convert_to_path(list_url) - md += f"- [{list_text}]({list_url})\n" + md += f"- [{escape_generics_in_link_text(list_text)}]({list_url})\n" else: md += f"- {list_text}\n" md += "\n" + if 'parameters' in item: for param in item['parameters']: param_name = param.get('name', '') param_default = param.get('default', '') - param_type = '' param_description = param.get('description', '') - parts = [] + + type_text, type_href = '', '' if 'type' in param: - for t in param['type']: - if isinstance(t, dict): - url = t.get('url', '') - if url.endswith('.html'): - url = "/docs/api/" + convert_to_path(url) - parts.append(f"[{t['text']}]({url})") - else: - if str(t) == "text": - parts.append(f"[{param['type']['text']}]") - elif str(t) == "url": - url = param['type']['url'] - if url.endswith('.html'): - url = "/docs/api/" + convert_to_path(url) - parts.append(f"({url})") - else: - parts.append(str(t)) - - if param_default != '': - parts.append(f"{param_default}") - if param_description != '': - parts.append(f" - {param_description}") - param_type = ''.join(parts) - md += f"- **{param_name}**: {param_type}\n" if param_name else f"- {param_type}\n" + type_text, type_href = format_type_link(param['type']) + + type_text_escaped = escape_generics(type_text) + + parts = [] + if param_name: + parts.append(f'name="{param_name}"') + if type_text_escaped: + parts.append(f'type="{type_text_escaped}"') + if type_href: + parts.append(f'typeHref="{type_href}"') + + api_param = f"" + + if param_description: + md += f"- {api_param} — {fix_html_for_jsx(param_description)}\n" + elif param_default != '': + md += f"- {api_param} = {param_default}\n" + else: + md += f"- {api_param}\n" md += "\n" + if 'api3' in item: src = item.get('src', '') api3_title = str(item.get('api3', '')) @@ -159,9 +228,11 @@ def generate_markdown(yaml_data): md += f"### {api3_title}\n\n" if src != '': src = src.replace('/blob/main', '/blob/master') - md += f"[View Source]({src})\n\n" + md += f'\n\n' + return md + def convert_yaml_file(src_path, dest_path): with open(src_path, 'r', encoding='utf-8') as f: yaml_data = yaml.safe_load(f) @@ -169,7 +240,6 @@ def convert_yaml_file(src_path, dest_path): if isinstance(yaml_data, list): yaml_data = yaml_data[0] - # Detect if it's an index.md (folder-style) folder_path, file_name = os.path.split(dest_path) base_name, ext = os.path.splitext(file_name) is_index = ( @@ -186,26 +256,31 @@ def convert_yaml_file(src_path, dest_path): md_content += generate_markdown(yaml_data) base_name = transform_filename(base_name) - final_path = os.path.join(folder_path, base_name + ".md") + final_path = os.path.join(folder_path, base_name + ".mdx") if is_index: final_folder = os.path.join(folder_path, base_name) os.makedirs(final_folder, exist_ok=True) - final_path = os.path.join(final_folder, "index.md") + final_path = os.path.join(final_folder, "index.mdx") else: os.makedirs(folder_path, exist_ok=True) with open(final_path, 'w', encoding='utf-8') as f: f.write(md_content) + for root, dirs, files in os.walk(SOURCE_DIR): for file in files: if file.endswith(".yml") or file.endswith(".yaml"): raw_base = os.path.splitext(file)[0] - # Merge Core and Shared paths - raw_base = raw_base.replace("SwiftlyS2.Core.", "SwiftlyS2.").replace("SwiftlyS2.Shared.", "SwiftlyS2.") + # Remove Core and Shared from path + for prefix in ["SwiftlyS2.Core.", "SwiftlyS2.Shared.", "SwiftlyS2."]: + if raw_base.startswith(prefix): + raw_base = raw_base[len(prefix):] + break new_base = transform_filename(raw_base) - dest_file = os.path.join(DEST_DIR, convert_to_path(new_base) + ".md") + dest_file = os.path.join(DEST_DIR, "/".join(new_base.split(".")).lower() + ".mdx") convert_yaml_file(os.path.join(root, file), dest_file) -print("Markdown generation complete!") + +print("MDX generation complete!") \ No newline at end of file