Passed
Push — master ( 63ff1a...3c0f18 )
by Jan
07:51 queued 14s
created

sphinxarg.markdown.literal()   B

Complexity

Conditions 8

Size

Total Lines 26
Code Lines 20

Duplication

Lines 26
Ratio 100 %

Importance

Changes 0
Metric Value
cc 8
eloc 20
nop 1
dl 26
loc 26
rs 7.3333
c 0
b 0
f 0
1
try:
2
    from commonmark import Parser
3
except ImportError:
4
    from CommonMark import Parser  # >= 0.5.6
5
try:
6
    from commonmark.node import Node
7
except ImportError:
8
    from CommonMark.node import Node
9
from docutils import nodes
10
from docutils.utils.code_analyzer import Lexer
11
12
13
def custom_walker(node, space=''):
14
    """
15
    A convenience function to ease debugging. It will print the node structure that's returned from CommonMark
16
17
    The usage would be something like:
18
19
    >>> content = Parser().parse('Some big text block\n===================\n\nwith content\n')
20
    >>> custom_walker(content)
21
    document
22
        heading
23
            text    Some big text block
24
        paragraph
25
            text    with content
26
27
    Spaces are used to convey nesting
28
    """
29
    txt = ''
30
    try:
31
        txt = node.literal
32
    except Exception:
33
        pass
34
35
    if txt is None or txt == '':
36
        print(f'{space}{node.t}')
37
    else:
38
        print(f'{space}{node.t}\t{txt}')
39
40
    cur = node.first_child
41
    if cur:
42
        while cur is not None:
43
            custom_walker(cur, space + '    ')
44
            cur = cur.nxt
45
46
47
def paragraph(node):
48
    """
49
    Process a paragraph, which includes all content under it
50
    """
51
    text = ''
52
    if node.string_content is not None:
53
        text = node.string_content
54
    o = nodes.paragraph('', ' '.join(text))
55
    o.line = node.sourcepos[0][0]
56
    for n in markdown(node):
57
        o.append(n)
58
59
    return o
60
61
62
def text(node):
63
    """
64
    Text in a paragraph
65
    """
66
    return nodes.Text(node.literal)
67
68
69
def hardbreak(node):
70
    """
71
    A <br /> in html or "\n" in ascii
72
    """
73
    return nodes.Text('\n')
74
75
76
def softbreak(node):
77
    """
78
    A line ending or space.
79
    """
80
    return nodes.Text('\n')
81
82
83
def reference(node):
84
    """
85
    A hyperlink. Note that alt text doesn't work, since there's no apparent way to do that in docutils
86
    """
87
    o = nodes.reference()
88
    o['refuri'] = node.destination
89
    if node.title:
90
        o['name'] = node.title
91
    for n in markdown(node):
92
        o += n
93
    return o
94
95
96
def emphasis(node):
97
    """
98
    An italicized section
99
    """
100
    o = nodes.emphasis()
101
    for n in markdown(node):
102
        o += n
103
    return o
104
105
106
def strong(node):
107
    """
108
    A bolded section
109
    """
110
    o = nodes.strong()
111
    for n in markdown(node):
112
        o += n
113
    return o
114
115
116
def literal(node):
117
    """
118
    Inline code
119
    """
120
    rendered = []
121
    try:
122
        if node.info is not None:
123
            l = Lexer(node.literal, node.info, tokennames="long")
124
            for _ in l:
125
                rendered.append(node.inline(classes=_[0], text=_[1]))
126
    except Exception:
127
        pass
128
129
    classes = ['code']
130
    if node.info is not None:
131
        classes.append(node.info)
132
    if len(rendered) > 0:
133
        o = nodes.literal(classes=classes)
134
        for element in rendered:
135
            o += element
136
    else:
137
        o = nodes.literal(text=node.literal, classes=classes)
138
139
    for n in markdown(node):
140
        o += n
141
    return o
142
143
144
def literal_block(node):
145
    """
146
    A block of code
147
    """
148
    rendered = []
149
    try:
150
        if node.info is not None:
151
            l = Lexer(node.literal, node.info, tokennames="long")
152
            for _ in l:
153
                rendered.append(node.inline(classes=_[0], text=_[1]))
154
    except Exception:
155
        pass
156
157
    classes = ['code']
158
    if node.info is not None:
159
        classes.append(node.info)
160
    if len(rendered) > 0:
161
        o = nodes.literal_block(classes=classes)
162
        for element in rendered:
163
            o += element
164
    else:
165
        o = nodes.literal_block(text=node.literal, classes=classes)
166
167
    o.line = node.sourcepos[0][0]
168
    for n in markdown(node):
169
        o += n
170
    return o
171
172
173
def raw(node):
174
    """
175
    Add some raw html (possibly as a block)
176
    """
177
    o = nodes.raw(node.literal, node.literal, format='html')
178
    if node.sourcepos is not None:
179
        o.line = node.sourcepos[0][0]
180
    for n in markdown(node):
181
        o += n
182
    return o
183
184
185
def transition(node):
186
    """
187
    An <hr> tag in html. This has no children
188
    """
189
    return nodes.transition()
190
191
192
def title(node):
193
    """
194
    A title node. It has no children
195
    """
196
    return nodes.title(node.first_child.literal, node.first_child.literal)
197
198
199
def section(node):
200
    """
201
    A section in reStructuredText, which needs a title (the first child)
202
    This is a custom type
203
    """
204
    title = ''  # All sections need an id
205
    if node.first_child is not None:
206
        if node.first_child.t == 'heading':
207
            title = node.first_child.first_child.literal
208
    o = nodes.section(ids=[title], names=[title])
209
    for n in markdown(node):
210
        o += n
211
    return o
212
213
214
def block_quote(node):
215
    """
216
    A block quote
217
    """
218
    o = nodes.block_quote()
219
    o.line = node.sourcepos[0][0]
220
    for n in markdown(node):
221
        o += n
222
    return o
223
224
225
def image(node):
226
    """
227
    An image element
228
229
    The first child is the alt text. reStructuredText can't handle titles
230
    """
231
    o = nodes.image(uri=node.destination)
232
    if node.first_child is not None:
233
        o['alt'] = node.first_child.literal
234
    return o
235
236
237
def list_item(node):
238
    """
239
    An item in a list
240
    """
241
    o = nodes.list_item()
242
    for n in markdown(node):
243
        o += n
244
    return o
245
246
247
def list_node(node):
248
    """
249
    A list (numbered or not)
250
    For numbered lists, the suffix is only rendered as . in html
251
    """
252
    if node.list_data['type'] == 'bullet':
253
        o = nodes.bullet_list(bullet=node.list_data['bullet_char'])
254
    else:
255
        o = nodes.enumerated_list(
256
            suffix=node.list_data['delimiter'],
257
            enumtype='arabic',
258
            start=node.list_data['start'],
259
        )
260
    for n in markdown(node):
261
        o += n
262
    return o
263
264
265
def markdown(node):
266
    """
267
    Returns a list of nodes, containing CommonMark nodes converted to docutils nodes
268
    """
269
    cur = node.first_child
270
271
    # Go into each child, in turn
272
    output = []
273
    while cur is not None:
274
        t = cur.t
275
        if t == 'paragraph':
276
            output.append(paragraph(cur))
277
        elif t == 'text':
278
            output.append(text(cur))
279
        elif t == 'softbreak':
280
            output.append(softbreak(cur))
281
        elif t == 'linebreak':
282
            output.append(hardbreak(cur))
283
        elif t == 'link':
284
            output.append(reference(cur))
285
        elif t == 'heading':
286
            output.append(title(cur))
287
        elif t == 'emph':
288
            output.append(emphasis(cur))
289
        elif t == 'strong':
290
            output.append(strong(cur))
291
        elif t == 'code':
292
            output.append(literal(cur))
293
        elif t == 'code_block':
294
            output.append(literal_block(cur))
295
        elif t == 'html_inline' or t == 'html_block':
296
            output.append(raw(cur))
297
        elif t == 'block_quote':
298
            output.append(block_quote(cur))
299
        elif t == 'thematic_break':
300
            output.append(transition(cur))
301
        elif t == 'image':
302
            output.append(image(cur))
303
        elif t == 'list':
304
            output.append(list_node(cur))
305
        elif t == 'item':
306
            output.append(list_item(cur))
307
        elif t == 'MDsection':
308
            output.append(section(cur))
309
        else:
310
            print(f'Received unhandled type: {t}. Full print of node:')
311
            cur.pretty()
312
313
        cur = cur.nxt
314
315
    return output
316
317
318
def finalize_section(section):
319
    """
320
    Correct the nxt and parent for each child
321
    """
322
    cur = section.first_child
323
    last = section.last_child
324
    if last is not None:
325
        last.nxt = None
326
327
    while cur is not None:
328
        cur.parent = section
329
        cur = cur.nxt
330
331
332
def nest_sections(block, level=1):
333
    """
334
    Sections aren't handled by CommonMark at the moment.
335
    This function adds sections to a block of nodes.
336
    'title' nodes with an assigned level below 'level' will be put in a child section.
337
    If there are no child nodes with titles of level 'level' then nothing is done
338
    """
339
    cur = block.first_child
340
    if cur is not None:
341
        children = []
342
        # Do we need to do anything?
343
        nest = False
344
        while cur is not None:
345
            if cur.t == 'heading' and cur.level == level:
346
                nest = True
347
                break
348
            cur = cur.nxt
349
        if not nest:
350
            return
351
352
        section = Node('MDsection', 0)
353
        section.parent = block
354
        cur = block.first_child
355
        while cur is not None:
356
            if cur.t == 'heading' and cur.level == level:
357
                # Found a split point, flush the last section if needed
358
                if section.first_child is not None:
359
                    finalize_section(section)
360
                    children.append(section)
361
                    section = Node('MDsection', 0)
362
            nxt = cur.nxt
363
            # Avoid adding sections without titles at the start
364
            if section.first_child is None:
365
                if cur.t == 'heading' and cur.level == level:
366
                    section.append_child(cur)
367
                else:
368
                    children.append(cur)
369
            else:
370
                section.append_child(cur)
371
            cur = nxt
372
373
        # If there's only 1 child then don't bother
374
        if section.first_child is not None:
375
            finalize_section(section)
376
            children.append(section)
377
378
        block.first_child = None
379
        block.last_child = None
380
        next_level = level + 1
381
        for child in children:
382
            # Handle nesting
383
            if child.t == 'MDsection':
384
                nest_sections(child, level=next_level)
385
386
            # Append
387
            if block.first_child is None:
388
                block.first_child = child
389
            else:
390
                block.last_child.nxt = child
391
            child.parent = block
392
            child.nxt = None
393
            child.prev = block.last_child
394
            block.last_child = child
395
396
397
def parse_markdown_block(text):
398
    """
399
    Parses a block of text, returning a list of docutils nodes
400
401
    >>> parse_markdown_block("Some\n====\n\nblock of text\n\nHeader\n======\n\nblah\n")
402
    []
403
    """
404
    block = Parser().parse(text)
405
    # CommonMark can't nest sections, so do it manually
406
    nest_sections(block)
407
408
    return markdown(block)
409