|
10 | 10 | """
|
11 | 11 |
|
12 | 12 | import re
|
13 |
| - |
14 | 13 | from pathlib import Path
|
15 | 14 | from sphinx.application import Sphinx
|
16 | 15 | from dataclasses import dataclass
|
17 | 16 |
|
18 |
| - |
19 | 17 | LINK_CORE = r"""
|
20 | 18 | \[ (?P<text>[^\[\]]*?) \] # link brackets + text w/o brackets - allows spaces in text
|
21 | 19 | \(
|
|
49 | 47 | re.VERBOSE, # whitespace and comments are ignored
|
50 | 48 | )
|
51 | 49 |
|
52 |
| - |
53 |
| -def redown(text: str) -> str: |
| 50 | +def redown(text: str): |
54 | 51 | """21"""
|
55 |
| - |
56 |
| - # replace md code blocks with reST code blocks |
57 |
| - "redown, redown, redown, redown" |
58 |
| - find = r"(?P<start>^|\n)(?P<btindent> *?)(?P<ticks>```+)(?P<lang>\S+) *?(?:\n\s*?)+(?P<cindent> *?)(?P<code>.*?)(?P=ticks) *?(?P<end>\n|$|\Z)" |
59 |
| - |
60 |
| - def replace(match: re.Match) -> str: |
61 |
| - start = match.group("start") |
62 |
| - btindent = match.group("btindent") |
63 |
| - lang: str = match.group("lang") |
64 |
| - cindent = match.group("cindent") |
65 |
| - code = match.group("code") |
66 |
| - end = match.group("end") |
67 |
| - |
68 |
| - ret = "" |
69 |
| - ret += start |
70 |
| - ret += btindent + f".. code-block:: {lang}\n" |
71 |
| - cindent = 3 * " " |
72 |
| - |
73 |
| - in_args = True |
74 |
| - |
75 |
| - for line in code.splitlines(keepends=True): |
76 |
| - if in_args and re.search(r"^\s*:.+:", line) and not re.match(r"re?st", lang.lower()): |
77 |
| - ret += cindent + line |
78 |
| - continue |
79 |
| - elif in_args: |
80 |
| - in_args = False |
81 |
| - ret += "\n" |
82 |
| - |
83 |
| - if line.strip() == "": |
84 |
| - ret += "\n" |
| 52 | + """Transforms markdown-like syntax into reStructuredText and maps input to output line numbers.""" |
| 53 | + input_lines = text.splitlines(keepends=True) |
| 54 | + output_lines = [] |
| 55 | + line_mapping = [] # List of input line numbers for each output line |
| 56 | + i = 0 # Input line index |
| 57 | + while i < len(input_lines): |
| 58 | + line = input_lines[i] |
| 59 | + # Check for code block start |
| 60 | + code_block_match = re.match(r'^( *)(`{3,})(\S+)?\s*$', line) |
| 61 | + if code_block_match: |
| 62 | + # Start of code block |
| 63 | + btindent = code_block_match.group(1) or '' |
| 64 | + lang = code_block_match.group(3) or '' |
| 65 | + is_rst: bool = re.search(r"re?st", lang.lower()) is not None |
| 66 | + |
| 67 | + params_lines = [] |
| 68 | + params_input_line_numbers = [] |
| 69 | + code_content_lines = [] |
| 70 | + code_content_input_line_numbers = [] |
| 71 | + i += 1 |
| 72 | + while i < len(input_lines) and not re.match(r'^( *)`{3,}\s*$', input_lines[i]): |
| 73 | + code_line = input_lines[i] |
| 74 | + if not is_rst and not code_content_lines and re.match(r'^\s*:.*:', code_line): |
| 75 | + # Parameter line |
| 76 | + params_lines.append(code_line) |
| 77 | + params_input_line_numbers.append(i) |
| 78 | + else: |
| 79 | + # Code content line |
| 80 | + code_content_lines.append(code_line) |
| 81 | + code_content_input_line_numbers.append(i) |
| 82 | + i += 1 |
| 83 | + if i < len(input_lines): |
| 84 | + # Closing ``` |
| 85 | + i += 1 |
| 86 | + # Now process the code block |
| 87 | + code_block_directive = f'{btindent}.. code-block::{" " + lang if lang else ""}\n' |
| 88 | + output_lines.append(code_block_directive) |
| 89 | + line_mapping.append(i - len(code_content_lines) - len(params_lines) - 1) # Map to the opening ``` |
| 90 | + # Add parameter lines immediately after the directive (bug 3) |
| 91 | + option_indent = btindent + ' ' |
| 92 | + for idx, param_line in enumerate(params_lines): |
| 93 | + output_lines.append(option_indent + param_line) |
| 94 | + line_mapping.append(params_input_line_numbers[idx]) |
| 95 | + # Add a blank line before the code content |
| 96 | + output_lines.append('\n') |
| 97 | + if params_input_line_numbers: |
| 98 | + line_mapping.append(params_input_line_numbers[-1]) # Map to last param line |
85 | 99 | else:
|
86 |
| - ret += cindent + line |
87 |
| - |
88 |
| - return ret |
89 |
| - |
90 |
| - code = lambda: re.sub(find, replace, text, flags=re.DOTALL) |
91 |
| - text = code() |
92 |
| - |
93 |
| - # find rst code block ranges |
94 |
| - "redown, redown, redown, redown" |
95 |
| - |
96 |
| - @dataclass |
97 |
| - class Chunk: |
98 |
| - is_code: bool |
99 |
| - text: str = "" |
100 |
| - |
101 |
| - chunks: list[Chunk] = [] |
102 |
| - |
103 |
| - for line in text.splitlines(keepends=True): |
104 |
| - in_code_block = chunks and chunks[-1].is_code |
105 |
| - is_code_block_directive = re.match(r"^\s*\.\. code-block::", line) |
106 |
| - |
107 |
| - if not in_code_block and is_code_block_directive: |
108 |
| - chunks.append(Chunk(is_code=True)) |
109 |
| - elif in_code_block: |
110 |
| - indent = len(re.match(r"^(\s*)", line).group(1).rstrip("\n")) |
111 |
| - code_block_indent = len( |
112 |
| - re.match(r"^(\s*)", chunks[-1].text).group(1).rstrip("\n") |
113 |
| - ) |
114 |
| - if len(line.strip()) and indent <= code_block_indent: |
115 |
| - if is_code_block_directive: |
116 |
| - chunks.append(Chunk(is_code=True)) |
| 100 | + line_mapping.append(i - len(code_content_lines) - 1) # Map to the opening ``` |
| 101 | + # Add code content lines |
| 102 | + for idx, code_line in enumerate(code_content_lines): |
| 103 | + # Handle bug 2: Don't indent empty lines |
| 104 | + if code_line.strip(): |
| 105 | + output_lines.append(' ' + code_line) |
117 | 106 | else:
|
118 |
| - chunks.append(Chunk(is_code=False)) |
119 |
| - |
120 |
| - if not chunks: |
121 |
| - chunks.append(Chunk(is_code=False)) |
122 |
| - chunks[-1].text += line # existing block |
123 |
| - |
124 |
| - # dont operate on code blocks |
125 |
| - for chunk in chunks: |
126 |
| - if chunk.is_code: |
127 |
| - continue |
128 |
| - text = chunk.text |
129 |
| - |
130 |
| - "redown, redown, redown, redown" |
131 |
| - heading = lambda prefix, underfix: re.sub( |
132 |
| - rf"(^|\n){prefix} +(.+?)(?:$|\n|\Z)", |
133 |
| - lambda m: f"{m.group(1)}{m.group(2)}\n{underfix * len(m.group(2))}\n", |
134 |
| - text, |
135 |
| - ) |
136 |
| - |
137 |
| - text = heading("#", "=") |
138 |
| - text = heading("##", "-") |
139 |
| - text = heading("###", "^") |
140 |
| - text = heading("####", "~") |
141 |
| - |
142 |
| - "redown, redown, redown, redown" |
143 |
| - role_links = lambda: ROLE_LINK_RE.sub( |
144 |
| - lambda m: f"{m.group('role')}`{(t:=m.group('text'))}{' ' if len(t) else ''}<{m.group('link')}>`", |
145 |
| - text, |
146 |
| - ) |
147 |
| - text = role_links() |
148 |
| - |
149 |
| - "redown, redown, redown, redown" |
150 |
| - links = lambda: LINK_RE.sub( |
151 |
| - lambda m: f"`{(t:=m.group('text'))}{' ' if len(t) else ''}<{m.group('link')}>`__", |
152 |
| - text, |
153 |
| - ) |
154 |
| - text = links() |
155 |
| - |
156 |
| - "redown, redown, redown, redown" |
157 |
| - math = lambda: re.sub( |
158 |
| - r"(\b|\s|^)\$([^$\n]+)\$(\b|\s|[^\w]|$)", r"\1:math:`\2`\3", text |
159 |
| - ) |
160 |
| - text = math() |
161 |
| - |
162 |
| - chunk.text = text |
163 |
| - |
164 |
| - text = "".join(chunk.text for chunk in chunks) |
165 |
| - |
166 |
| - return text |
167 |
| - |
| 107 | + output_lines.append('\n') |
| 108 | + line_mapping.append(code_content_input_line_numbers[idx]) |
| 109 | + # Add a blank line after the code content |
| 110 | + if code_content_lines: |
| 111 | + output_lines.append('\n') |
| 112 | + line_mapping.append(code_content_input_line_numbers[idx]) |
| 113 | + else: |
| 114 | + # Process headings |
| 115 | + heading_match = re.match(r'^(#+) (.+)$', line) |
| 116 | + if heading_match: |
| 117 | + level = len(heading_match.group(1)) |
| 118 | + heading_text = heading_match.group(2).strip() |
| 119 | + if level == 1: |
| 120 | + underline = '=' * len(heading_text) |
| 121 | + elif level == 2: |
| 122 | + underline = '-' * len(heading_text) |
| 123 | + elif level == 3: |
| 124 | + underline = '^' * len(heading_text) |
| 125 | + elif level == 4: |
| 126 | + underline = '~' * len(heading_text) |
| 127 | + else: |
| 128 | + underline = '"' * len(heading_text) |
| 129 | + output_lines.append(heading_text + '\n') |
| 130 | + line_mapping.append(i) |
| 131 | + output_lines.append(underline + '\n') |
| 132 | + line_mapping.extend([i, i]) # Underline and extra newline map to the same input line |
| 133 | + else: |
| 134 | + # Process role links, regular links, and inline math |
| 135 | + line_processed = ROLE_LINK_RE.sub( |
| 136 | + lambda m: f"{m.group('role')}`{(t:=m.group('text'))}{' ' if len(t) else ''}<{m.group('link')}>`", |
| 137 | + line) |
| 138 | + line_processed = LINK_RE.sub( |
| 139 | + lambda m: f"`{(t:=m.group('text'))}{' ' if len(t) else ''}<{m.group('link')}>`__", |
| 140 | + line_processed) |
| 141 | + line_processed = re.sub( |
| 142 | + r"(\b|\s|^)\$([^$\n]+)\$(\b|\s|[^\w]|$)", r"\1:math:`\2`\3", line_processed) |
| 143 | + output_lines.append(line_processed) |
| 144 | + line_mapping.append(i) |
| 145 | + i += 1 |
| 146 | + output_text = ''.join(output_lines) |
| 147 | + return output_text, line_mapping |
| 148 | + "redown, redown, redown, redown" |
168 | 149 |
|
169 | 150 | def setup(app: Sphinx):
|
170 | 151 | @(lambda breadcrumb: app.connect("source-read", breadcrumb))
|
171 | 152 | def _(app, docname, content):
|
172 |
| - content[0] = redown(content[0]) |
| 153 | + content[0] = redown(content[0])[0] |
173 | 154 | # Path(app.srcdir, docname).with_suffix(".rd").write_text(content[0], encoding="utf8")
|
174 | 155 |
|
175 | 156 | return {
|
|
0 commit comments