24
24
from docutils .parsers .rst .directives import unchanged as directive_param_unchanged
25
25
from docutils .utils import Reporter , SystemMessage
26
26
27
+ _SPECIAL_ATTRIBUTES = (
28
+ "antsibull-code-language" ,
29
+ "antsibull-code-block" ,
30
+ "antsibull-code-lineno" ,
31
+ )
32
+
27
33
28
34
class IgnoreDirective (Directive ):
29
35
"""
@@ -37,14 +43,24 @@ def run(self) -> list:
37
43
38
44
39
45
def mark_antsibull_code_block (
40
- node : nodes .literal_block , * , language : str | None , line : int
46
+ node : nodes .literal_block ,
47
+ * ,
48
+ language : str | None ,
49
+ line : int ,
50
+ other : dict [str , t .Any ] | None = None ,
41
51
) -> None :
42
52
"""
43
53
Mark a literal block as an Antsibull code block with given language and line number.
54
+
55
+ Everything in ``other`` will be available as ``antsibull-other-{key}`` for a key ``key``
56
+ in ``other`` in the node's attributes.
44
57
"""
45
58
node ["antsibull-code-language" ] = language
46
59
node ["antsibull-code-block" ] = True
47
60
node ["antsibull-code-lineno" ] = line
61
+ if other :
62
+ for key , value in other .items ():
63
+ node [f"antsibull-other-{ key } " ] = value
48
64
49
65
50
66
class CodeBlockDirective (Directive ):
@@ -92,7 +108,9 @@ def __init__(
92
108
self ,
93
109
document : nodes .document ,
94
110
content : str ,
95
- callback : t .Callable [[str , int , int , bool , str ], None ],
111
+ callback : t .Callable [
112
+ [str , int , int , bool , bool , str , nodes .literal_block ], None
113
+ ],
96
114
warn_unknown_block : t .Callable [[int | str , int , nodes .literal_block ], None ],
97
115
):
98
116
super ().__init__ (document )
@@ -114,6 +132,12 @@ def visit_error(self, node: nodes.error) -> None:
114
132
115
133
@staticmethod
116
134
def _find_indent (content : str ) -> int | None :
135
+ """
136
+ Given concatenated lines, find the minimum indent if possible.
137
+
138
+ If all lines consist only out of whitespace (or are empty),
139
+ ``None`` is returned.
140
+ """
117
141
min_indent = None
118
142
for line in content .split ("\n " ):
119
143
stripped_line = line .lstrip ()
@@ -123,7 +147,14 @@ def _find_indent(content: str) -> int | None:
123
147
min_indent = indent
124
148
return min_indent
125
149
126
- def _find_offset (self , lineno : int , content : str ) -> tuple [int , int ]:
150
+ def _find_offset (self , lineno : int , content : str ) -> tuple [int , int , bool ]:
151
+ """
152
+ Try to identify the row/col offset of the code in ``content`` in the document.
153
+
154
+ ``lineno`` is assumed to be the line where the code-block starts.
155
+ This function looks for an empty line, followed by the right pattern of
156
+ empty and non-empty lines.
157
+ """
127
158
row_offset = lineno
128
159
found_empty_line = False
129
160
found_content_lines = False
@@ -150,9 +181,15 @@ def _find_offset(self, lineno: int, content: str) -> tuple[int, int]:
150
181
151
182
min_source_indent = self ._find_indent (content )
152
183
col_offset = max (0 , (min_indent or 0 ) - (min_source_indent or 0 ))
153
- return row_offset , col_offset
184
+ return row_offset , col_offset , content_lines == 0
154
185
155
186
def _find_in_code (self , row_offset : int , col_offset : int , content : str ) -> bool :
187
+ """
188
+ Check whether the code can be found at the given row/col offset in a way
189
+ that makes it easy to replace.
190
+
191
+ That is, it is surrounded only by whitespace.
192
+ """
156
193
for index , line in enumerate (content .split ("\n " )):
157
194
if row_offset + index >= len (self .__content_lines ):
158
195
return False
@@ -175,16 +212,33 @@ def visit_literal_block(self, node: nodes.literal_block) -> None:
175
212
# This could be a `::` block, or something else (unknown)
176
213
self .__warn_unknown_block (node .line or "unknown" , 0 , node )
177
214
raise nodes .SkipNode
215
+ print (node .attributes )
178
216
179
217
language = node .attributes ["antsibull-code-language" ]
180
218
lineno = node .attributes ["antsibull-code-lineno" ]
181
- row_offset , col_offset = self ._find_offset (lineno , node .rawsource )
219
+ row_offset , col_offset , position_exact = self ._find_offset (
220
+ lineno , node .rawsource
221
+ )
222
+ found_in_code = False
223
+ if position_exact :
224
+ # If we think we have the exact position, try to identify the code.
225
+ # ``found_in_code`` indicates that it is easy to replace the code,
226
+ # and at the same time it's easy to identify it.
227
+ found_in_code = self ._find_in_code (row_offset , col_offset , node .rawsource )
228
+ if not found_in_code :
229
+ position_exact = False
230
+ if not found_in_code :
231
+ # We were not able to find hte code 'the easy way'. This could be because
232
+ # it is inside a table.
233
+ pass # TODO search for the content, f.ex. in tables
182
234
self .__callback (
183
235
language ,
184
236
row_offset ,
185
237
col_offset ,
186
- self ._find_in_code (row_offset , col_offset , node .rawsource ),
238
+ position_exact ,
239
+ found_in_code ,
187
240
node .rawsource .rstrip () + "\n " ,
241
+ node ,
188
242
)
189
243
raise nodes .SkipNode
190
244
@@ -253,13 +307,22 @@ class CodeBlockInfo:
253
307
row_offset : int
254
308
col_offset : int
255
309
310
+ # Whether the position (row/col_offset) is exact.
311
+ # If set to ``False``, the position is approximate and col_offset is often 0.
312
+ position_exact : bool
313
+
256
314
# Whether the code block's contents can be found as-is in the RST file,
257
315
# only indented by whitespace, and with potentially trailing whitespace
258
- directly_in_content : bool
316
+ directly_replacable_in_content : bool
259
317
260
318
# The code block's contents
261
319
content : str
262
320
321
+ # The code block's attributes that start with ``antsibull-``.
322
+ # Special attributes used by ``find_code_blocks()`` to keep track of
323
+ # certain properties are not present.
324
+ attributes : dict [str , t .Any ]
325
+
263
326
264
327
def find_code_blocks (
265
328
content : str ,
@@ -284,20 +347,28 @@ def find_code_blocks(
284
347
# using this ugly list
285
348
results = []
286
349
287
- def callback (
350
+ def callback ( # pylint: disable=too-many-arguments,too-many-positional-arguments
288
351
language : str ,
289
352
row_offset : int ,
290
353
col_offset : int ,
291
- directly_in_content : bool ,
354
+ position_exact : bool ,
355
+ directly_replacable_in_content : bool ,
292
356
content : str ,
357
+ node : nodes .literal_block ,
293
358
) -> None :
294
359
results .append (
295
360
CodeBlockInfo (
296
361
language = language ,
297
362
row_offset = row_offset ,
298
363
col_offset = col_offset ,
299
- directly_in_content = directly_in_content ,
364
+ position_exact = position_exact ,
365
+ directly_replacable_in_content = directly_replacable_in_content ,
300
366
content = content ,
367
+ attributes = {
368
+ key : value
369
+ for key , value in node .attributes .items ()
370
+ if key not in _SPECIAL_ATTRIBUTES and key .startswith ("antsibull-" )
371
+ },
301
372
)
302
373
)
303
374
0 commit comments