|
| 1 | +import os |
| 2 | +import re |
| 3 | + |
| 4 | +def convert_rst_to_md(filename, content): |
| 5 | + # Rule 6: Determine module name |
| 6 | + base_name = os.path.splitext(os.path.basename(filename))[0] |
| 7 | + module_name = f"firebird.base.{base_name}" |
| 8 | + |
| 9 | + # Overrides for some files |
| 10 | + if base_name in ['index', 'introduction', 'changelog', 'license', 'modules']: |
| 11 | + module_name = "firebird.base" |
| 12 | + |
| 13 | + lines = content.splitlines() |
| 14 | + new_lines = [] |
| 15 | + i = 0 |
| 16 | + |
| 17 | + def process_roles(text): |
| 18 | + def replace_role(match): |
| 19 | + role = match.group(1) |
| 20 | + content = match.group(2) |
| 21 | + display_content = content |
| 22 | + if display_content.startswith('~'): |
| 23 | + display_content = display_content[1:].split('.')[-1] |
| 24 | + |
| 25 | + if role == 'doc': |
| 26 | + return f"[{display_content.capitalize()}]({display_content}.md)" |
| 27 | + return f"`{display_content}`" |
| 28 | + |
| 29 | + return re.sub(r':(doc|ref|class|func|obj|mod|attr|exc):`([^`]+)`', replace_role, text) |
| 30 | + |
| 31 | + while i < len(lines): |
| 32 | + line = lines[i] |
| 33 | + |
| 34 | + # Skip module/synopsis |
| 35 | + if line.strip().startswith('.. module::') or line.strip().startswith(':synopsis:'): |
| 36 | + i += 1 |
| 37 | + continue |
| 38 | + |
| 39 | + # Rule 1: Headers |
| 40 | + # Title with overline and underline |
| 41 | + if i + 2 < len(lines) and re.match(r'^[#*=+^"~-]+$', line.strip()) and \ |
| 42 | + re.match(r'^[#*=+^"~-]+$', lines[i+2].strip()) and \ |
| 43 | + len(line.strip()) >= len(lines[i+1].strip()) and len(lines[i+2].strip()) >= len(lines[i+1].strip()): |
| 44 | + new_lines.append(f"# {lines[i+1].strip()}") |
| 45 | + i += 3 |
| 46 | + continue |
| 47 | + |
| 48 | + # Header with underline |
| 49 | + if i + 1 < len(lines) and re.match(r'^[#*=+^"~-]+$', lines[i+1].strip()) and \ |
| 50 | + len(lines[i+1].strip()) >= len(line.strip()) and len(line.strip()) > 0: |
| 51 | + char = lines[i+1].strip()[0] |
| 52 | + level = 2 |
| 53 | + if char == '#': level = 1 |
| 54 | + elif char == '=': level = 2 |
| 55 | + elif char == '-': level = 3 |
| 56 | + elif char == '^': level = 4 |
| 57 | + elif char == '"': level = 5 |
| 58 | + elif char == '~': level = 6 |
| 59 | + new_lines.append(f"{'#' * level} {line.strip()}") |
| 60 | + i += 2 |
| 61 | + continue |
| 62 | + |
| 63 | + # Rule 2: code-block |
| 64 | + if line.strip().startswith('.. code-block::'): |
| 65 | + lang = line.split('::')[-1].strip() |
| 66 | + new_lines.append(f"```{lang}") |
| 67 | + i += 1 |
| 68 | + # Skip empty lines until indented block |
| 69 | + while i < len(lines) and not lines[i].strip(): |
| 70 | + i += 1 |
| 71 | + # Find indentation |
| 72 | + if i < len(lines): |
| 73 | + indent = len(lines[i]) - len(lines[i].lstrip()) |
| 74 | + while i < len(lines) and (not lines[i].strip() or len(lines[i]) - len(lines[i].lstrip()) >= indent): |
| 75 | + new_lines.append(lines[i][indent:]) |
| 76 | + i += 1 |
| 77 | + new_lines.append("```") |
| 78 | + continue |
| 79 | + |
| 80 | + # Rule 3: Admonitions |
| 81 | + admonitions = ['note', 'tip', 'warning', 'important', 'caution', 'seealso', 'admonition', 'error', 'danger'] |
| 82 | + match_adm = re.match(r'^\.\.\s+(' + '|'.join(admonitions) + r')::(.*)', line.strip()) |
| 83 | + if match_adm: |
| 84 | + adm_type = match_adm.group(1) |
| 85 | + adm_text = match_adm.group(2).strip() |
| 86 | + |
| 87 | + if adm_type == 'admonition': |
| 88 | + new_lines.append(f"!!! {adm_text}") |
| 89 | + i += 1 |
| 90 | + else: |
| 91 | + new_lines.append(f"!!! {adm_type}") |
| 92 | + if adm_text: |
| 93 | + new_lines.append(f" {process_roles(adm_text)}") |
| 94 | + i += 1 |
| 95 | + |
| 96 | + # Skip empty lines |
| 97 | + while i < len(lines) and not lines[i].strip(): |
| 98 | + i += 1 |
| 99 | + if i < len(lines): |
| 100 | + curr_indent = len(lines[i]) - len(lines[i].lstrip()) |
| 101 | + if curr_indent > 0: |
| 102 | + indent = curr_indent |
| 103 | + while i < len(lines) and (not lines[i].strip() or len(lines[i]) - len(lines[i].lstrip()) >= indent): |
| 104 | + content_line = lines[i][indent:] |
| 105 | + content_line = process_roles(content_line) |
| 106 | + content_line = re.sub(r'^\.\.\s+(' + '|'.join(admonitions) + r')::', r'!!! \1', content_line) |
| 107 | + new_lines.append(f" {content_line}") |
| 108 | + i += 1 |
| 109 | + continue |
| 110 | + |
| 111 | + # Rule 5: mkdocstrings |
| 112 | + match_auto = re.match(r'^\.\.\s+auto(class|function|exception|data)::\s+([\w\.]+)', line.strip()) |
| 113 | + if match_auto: |
| 114 | + obj_type = match_auto.group(1) |
| 115 | + obj_name = match_auto.group(2) |
| 116 | + full_name = obj_name if '.' in obj_name else f"{module_name}.{obj_name}" |
| 117 | + new_lines.append(f"::: {full_name}") |
| 118 | + i += 1 |
| 119 | + # Skip options |
| 120 | + while i < len(lines) and (not lines[i].strip() or (len(lines[i]) - len(lines[i].lstrip()) > 0 and lines[i].strip().startswith(':'))): |
| 121 | + i += 1 |
| 122 | + continue |
| 123 | + |
| 124 | + # Rule 4 & 7: Sphinx roles and toctree |
| 125 | + line = process_roles(line) |
| 126 | + |
| 127 | + if line.strip().startswith('.. toctree::'): |
| 128 | + i += 1 |
| 129 | + while i < len(lines) and (not lines[i].strip() or lines[i].strip().startswith(':')): |
| 130 | + i += 1 |
| 131 | + while i < len(lines) and lines[i].strip() and not lines[i].strip().startswith('.'): |
| 132 | + fname = lines[i].strip() |
| 133 | + new_lines.append(f"- [{fname.capitalize()}]({fname}.md)") |
| 134 | + i += 1 |
| 135 | + continue |
| 136 | + |
| 137 | + # Rule 8: license include |
| 138 | + if line.strip().startswith('.. include::'): |
| 139 | + inc_path = line.split('::')[-1].strip() |
| 140 | + if 'LICENSE' in inc_path: |
| 141 | + try: |
| 142 | + with open('LICENSE', 'r') as f: |
| 143 | + new_lines.extend(f.read().splitlines()) |
| 144 | + except: |
| 145 | + new_lines.append(f"(Include {inc_path})") |
| 146 | + i += 1 |
| 147 | + continue |
| 148 | + |
| 149 | + # Horizontal rules |
| 150 | + if re.match(r'^[#*=+^"~-]{3,}$', line.strip()): |
| 151 | + new_lines.append("---") |
| 152 | + i += 1 |
| 153 | + continue |
| 154 | + |
| 155 | + # Handle simple links like `Firebird Project`_ |
| 156 | + line = re.sub(r'`([^`]+)`_', r'\1', line) |
| 157 | + # Handle link definitions |
| 158 | + match_link_def = re.match(r'^\.\.\s+_([^:]+):\s+(.*)', line.strip()) |
| 159 | + if match_link_def: |
| 160 | + new_lines.append(f"[{match_link_def.group(1)}]: {match_link_def.group(2)}") |
| 161 | + i += 1 |
| 162 | + continue |
| 163 | + |
| 164 | + if line.strip().startswith('.. |'): |
| 165 | + i += 1 |
| 166 | + continue |
| 167 | + |
| 168 | + if line.strip() == '|': |
| 169 | + new_lines.append("") |
| 170 | + i += 1 |
| 171 | + continue |
| 172 | + |
| 173 | + new_lines.append(line) |
| 174 | + i += 1 |
| 175 | + |
| 176 | + return "\n".join(new_lines) |
| 177 | + |
| 178 | +files_to_convert = [ |
| 179 | + 'buffer.txt', 'changelog.txt', 'collections.txt', 'config.txt', |
| 180 | + 'hooks.txt', 'index.txt', 'introduction.txt', 'license.txt', |
| 181 | + 'logging.txt', 'modules.txt', 'protobuf.txt', 'signal.txt', |
| 182 | + 'strconv.txt', 'trace.txt', 'types.txt' |
| 183 | +] |
| 184 | + |
| 185 | +for filename in files_to_convert: |
| 186 | + src_path = os.path.join('docs', filename) |
| 187 | + dest_path = os.path.join('zendocs/docs', filename.replace('.txt', '.md')) |
| 188 | + |
| 189 | + if os.path.exists(src_path): |
| 190 | + with open(src_path, 'r') as f: |
| 191 | + content = f.read() |
| 192 | + |
| 193 | + converted = convert_rst_to_md(filename, content) |
| 194 | + |
| 195 | + with open(dest_path, 'w') as f: |
| 196 | + f.write(converted) |
| 197 | + print(f"Converted {src_path} to {dest_path}") |
| 198 | + else: |
| 199 | + print(f"Skipping {src_path}, not found.") |
0 commit comments