Completed
Push — develop ( c2cafd...3dfc07 )
by Jace
24s queued 14s
created

doorstop.core.publisher_latex._matrix_latex()   B

Complexity

Conditions 7

Size

Total Lines 58
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 49
dl 0
loc 58
rs 7.269
c 0
b 0
f 0
cc 7
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
# SPDX-License-Identifier: LGPL-3.0-only
2
3
"""Functions to publish LaTeX documents."""
4
5
import os
6
import re
7
8
from doorstop import common, settings
9
from doorstop.common import DoorstopError
10
from doorstop.core.types import is_item, iter_items
11
12
log = common.logger(__name__)
13
14
15
def _lines_latex(obj, **kwargs):
16
    """Yield lines for a LaTeX report.
17
18
    :param obj: Item, list of Items, or Document to publish
19
    :param linkify: turn links into hyperlinks
20
21
    :return: iterator of lines of text
22
23
    """
24
    linkify = kwargs.get("linkify", False)
25
    for item in iter_items(obj):
26
        heading = "\\" + "sub" * (item.depth - 1) + "section*{"
27
        headingLev = "\\" + "sub" * (item.depth - 1) + "section{"
28
29
        if item.heading:
30
            text_lines = item.text.splitlines()
31
            # Level and Text
32
            if settings.PUBLISH_HEADING_LEVELS:
33
                standard = "{h}{t}{he}".format(
34
                    h=headingLev, t=text_lines[0] if text_lines else "", he="}"
35
                )
36
            else:
37
                standard = "{h}{t}{he}".format(
38
                    h=heading, t=text_lines[0] if text_lines else "", he="}"
39
                )
40
            attr_list = _format_latex_attr_list(item, True)
41
            yield standard + attr_list
42
            yield from text_lines[1:]
43
        else:
44
            uid = item.uid
45
            if settings.ENABLE_HEADERS:
46
                if item.header:
47
                    uid = "{h}{{\\small{{}}{u}}}".format(h=item.header, u=item.uid)
48
                else:
49
                    uid = "{u}".format(u=item.uid)
50
51
            # Level and UID
52
            if settings.PUBLISH_BODY_LEVELS:
53
                standard = "{h}{u}{he}".format(h=headingLev, u=uid, he="}")
54
            else:
55
                standard = "{h}{u}{he}".format(h=heading, u=uid, he="}")
56
57
            attr_list = _format_latex_attr_list(item, True)
58
            yield standard + attr_list
59
60
            # Text
61
            if item.text:
62
                yield ""  # break before text
63
                yield from _format_latex_text(item.text.splitlines())
64
65
            # Reference
66
            if item.ref:
67
                yield ""  # break before reference
68
                yield _format_latex_ref(item)
69
70
            # Reference
71
            if item.references:
72
                yield ""  # break before reference
73
                yield _format_latex_references(item)
74
75
            # Parent links
76
            if item.links:
77
                yield ""  # break before links
78
                items2 = item.parent_items
79
                if settings.PUBLISH_CHILD_LINKS:
80
                    label = "Parent links:"
81
                else:
82
                    label = "Links:"
83
                links = _format_latex_links(items2, linkify)
84
                label_links = _format_latex_label_links(label, links, linkify)
85
                yield label_links
86
87
            # Child links
88
            if settings.PUBLISH_CHILD_LINKS:
89
                items2 = item.find_child_items()
90
                if items2:
91
                    yield ""  # break before links
92
                    label = "Child links:"
93
                    links = _format_latex_links(items2, linkify)
94
                    label_links = _format_latex_label_links(label, links, linkify)
95
                    yield label_links
96
97
            # Add custom publish attributes
98 View Code Duplication
            if item.document and item.document.publish:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
99
                header_printed = False
100
                for attr in item.document.publish:
101
                    if not item.attribute(attr):
102
                        continue
103
                    if not header_printed:
104
                        header_printed = True
105
                        yield "\\begin{longtable}{|l|l|}"
106
                        yield "Attribute & Value\\\\"
107
                        yield "\\hline"
108
                    yield "{} & {}".format(attr, item.attribute(attr))
109
                if header_printed:
110
                    yield "\\end{longtable}"
111
                else:
112
                    yield ""
113
114
        yield ""  # break between items
115
116
117
def _format_latex_attr_list(item, linkify):
118
    """Create a LaTeX attribute list for a heading."""
119
    return (
120
        "{l}{u}{le}{zl}{u}{le}".format(l="\\label{", zl="\\zlabel{", u=item.uid, le="}")
121
        if linkify
122
        else ""
123
    )
124
125
126
def _format_latex_ref(item):
127
    """Format an external reference in LaTeX."""
128
    if settings.CHECK_REF:
129
        path, line = item.find_ref()
130
        path = path.replace("\\", "/")  # always use unix-style paths
131
        if line:
132
            return "\\begin{{quote}} \\verb|{p}| (line {line})\\end{{quote}}".format(
133
                p=path, line=line
134
            )
135
        else:
136
            return "\\begin{{quote}} \\verb|{p}|\\end{{quote}}".format(p=path)
137
    else:
138
        return "\\begin{{quote}} \\verb|{r}|\\end{{quote}}".format(r=item.ref)
139
140
141 View Code Duplication
def _format_latex_references(item):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
142
    """Format an external reference in LaTeX."""
143
    if settings.CHECK_REF:
144
        references = item.find_references()
145
        text_refs = []
146
        for ref_item in references:
147
            path, line = ref_item
148
            path = path.replace("\\", "/")  # always use unix-style paths
149
150
            if line:
151
                text_refs.append(
152
                    "\\begin{{quote}} \\verb|{p}| (line {line})\\end{{quote}}".format(
153
                        p=path, line=line
154
                    )
155
                )
156
            else:
157
                text_refs.append(
158
                    "\\begin{{quote}} \\verb|{p}|\\end{{quote}}".format(p=path)
159
                )
160
161
        return "\n".join(ref for ref in text_refs)
162
    else:
163
        references = item.references
164
        text_refs = []
165
        for ref_item in references:
166
            path = ref_item["path"]
167
            path = path.replace("\\", "/")  # always use unix-style paths
168
            text_refs.append(
169
                "\\begin{{quote}} \\verb|{r}|\\end{{quote}}".format(r=path)
170
            )
171
        return "\n".join(ref for ref in text_refs)
172
173
174
def _format_latex_links(items, linkify):
175
    """Format a list of linked items in LaTeX."""
176
    links = []
177
    for item in items:
178
        link = _format_latex_item_link(item, linkify=linkify)
179
        links.append(link)
180
    return ", ".join(links)
181
182
183
def _format_latex_item_link(item, linkify=True):
184
    """Format an item link in LaTeX."""
185
    if linkify and is_item(item):
186
        if item.header:
187
            return "\\hyperref[{u}]{{{u}}}".format(u=item.uid)
188
        return "\\hyperref[{u}]{{{u}}}".format(u=item.uid)
189
    else:
190
        return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
191
192
193
def _format_latex_label_links(label, links, linkify):
194
    """Join a string of label and links with formatting."""
195
    if linkify:
196
        return "\\textbf{{{lb}}} {ls}".format(lb=label, ls=links)
197
    else:
198
        return "\\textbf{{{lb} {ls}}}".format(lb=label, ls=links)
199
200
201
def _latex_convert(line):
202
    """Single string conversion for LaTeX."""
203
    # Replace $.
204
    line = re.sub("\\$", "\\\\$", line)
205
    #############################
206
    ## Fix BOLD and ITALICS and Strikethrough.
207
    #############################
208
    # Replace **.
209
    line = re.sub("\\*\\*(.*)\\*\\*", "\\\\textbf{\\1}", line)
210
    # Replace __.
211
    line = re.sub("__(.*)__", "\\\\textbf{\\1}", line)
212
    # Replace *.
213
    line = re.sub("\\*(.*)\\*", "\\\\textit{\\1}", line)
214
    # Replace _.
215
    line = re.sub("_(.*)_", "\\\\textit{\\1}", line)
216
    # Replace ~~.
217
    line = re.sub("~~(.*)~~", "\\\\sout{\\1}", line)
218
    #############################
219
    ## Fix manual heading levels
220
    #############################
221
    if settings.PUBLISH_BODY_LEVELS:
222
        star = ""
223
    else:
224
        star = "*"
225
    # Replace ######.
226
    line = re.sub(
227
        "###### (.*)",
228
        "\\\\subparagraph" + star + "{\\1 \\\\textbf{NOTE: This level is too deep.}}",
229
        line,
230
    )
231
    # Replace #####.
232
    line = re.sub("##### (.*)", "\\\\subparagraph" + star + "{\\1}", line)
233
    # Replace ####.
234
    line = re.sub("#### (.*)", "\\\\paragraph" + star + "{\\1}", line)
235
    # Replace ###.
236
    line = re.sub("### (.*)", "\\\\subsubsection" + star + "{\\1}", line)
237
    # Replace ##.
238
    line = re.sub("## (.*)", "\\\\subsection" + star + "{\\1}", line)
239
    # Replace #.
240
    line = re.sub("# (.*)", "\\\\section" + star + "{\\1}", line)
241
    return line
242
243
244
def _format_latex_text(text):
245
    """Fix all general text formatting to use LaTeX-macros."""
246
    block = []
247
    tableFound = False
248
    headerDone = False
249
    codeFound = False
250
    mathFound = False
251
    plantUMLFound = False
252
    enumerationFound = False
253
    itemizeFound = False
254
    for i, line in enumerate(text):
255
        noParagraph = False
256
        #############################
257
        ## Fix images.
258
        #############################
259
        image_match = re.findall(r"!\[(.*)\]\((.*)\)", line)
260
        if image_match:
261
            image_title, image_path = image_match[0]
262
            # Check for title. If not found, alt_text will be used as caption.
263
            title_match = re.findall(r'(.*)\s+"(.*)"', image_path)
264
            if title_match:
265
                image_path, image_title = title_match[0]
266
            # Make a safe label.
267
            label = "fig:{l}".format(l=re.sub("[^0-9a-zA-Z]+", "", image_title))
268
            # Make the string to replace!
269
            replacement = (
270
                r"\includegraphics[width=0.8\textwidth]{"
271
                + image_path
272
                + r"}}\label{{{l}}}\zlabel{{{l}}}".format(l=label)
273
                + r"\caption{"
274
                + _latex_convert(image_title)
275
                + r"}"
276
            ).replace("\\", "\\\\")
277
            # Replace with LaTeX format.
278
            line = re.sub(
279
                r"!\[(.*)\]\((.*)\)",
280
                replacement,
281
                line,
282
            )
283
            # Create the figure.
284
            block.append(r"\begin{figure}[h!]\center")
285
            block.append(line)
286
            line = r"\end{figure}"
287
288
        #############################
289
        ## Fix $ and MATH.
290
        #############################
291
        math_match = re.split("\\$\\$", line)
292
        if len(math_match) > 1:
293
            if mathFound and len(math_match) == 2:
294
                mathFound = False
295
                line = math_match[0] + "$" + _latex_convert(math_match[1])
296
            elif len(math_match) == 2:
297
                mathFound = True
298
                line = _latex_convert(math_match[0]) + "$" + math_match[1]
299
            elif len(math_match) == 3:
300
                line = (
301
                    _latex_convert(math_match[0])
302
                    + "$"
303
                    + math_match[1]
304
                    + "$"
305
                    + _latex_convert(math_match[2])
306
                )
307
            else:
308
                raise DoorstopError(
309
                    "Cannot handle multiple math environments on one row."
310
                )
311
        else:
312
            line = _latex_convert(line)
313
        # Skip all other changes if in MATH!
314
        if mathFound:
315
            line = line + "\\\\"
316
            block.append(line)
317
            continue
318
        #############################
319
        ## Fix code blocks.
320
        #############################
321
        code_match = re.findall("```", line)
322
        if codeFound:
323
            noParagraph = True
324
        if code_match:
325
            if codeFound:
326
                block.append("\\end{lstlisting}")
327
                codeFound = False
328
            else:
329
                block.append("\\begin{lstlisting}")
330
                codeFound = True
331
            # Replace ```.
332
            line = re.sub("```", "", line)
333
        # Replace ` for inline code.
334
        line = re.sub("`(.*)`", "\\\\lstinline`\\1`", line)
335
        #############################
336
        ## Fix enumeration.
337
        #############################
338
        enumeration_match = re.findall("^[0-9]+\\.\\s(.*)", line)
339
        if enumeration_match and not enumerationFound:
340
            block.append("\\begin{enumerate}")
341
            enumerationFound = True
342
        if enumerationFound:
343
            noParagraph = True
344
            if enumeration_match:
345
                # Replace the number.
346
                line = re.sub("^[0-9]+\\.\\s", "\\\\item ", line)
347
                # Look ahead - need empty line to end enumeration!
348
                if i < len(text) - 1:
349
                    nextLine = text[i + 1]
350
                    if nextLine == "":
351
                        block.append(line)
352
                        line = "\\end{enumerate}"
353
                        enumerationFound = False
354
            else:
355
                # Look ahead - need empty line to end enumeration!
356
                if i < len(text) - 1:
357
                    nextLine = text[i + 1]
358
                    if nextLine == "":
359
                        block.append(line)
360
                        line = "\\end{enumerate}"
361
                        enumerationFound = False
362
        #############################
363
        ## Fix itemize.
364
        #############################
365
        itemize_match = re.findall("^[\\*+-]\\s(.*)", line)
366
        if itemize_match and not itemizeFound:
367
            block.append("\\begin{itemize}")
368
            itemizeFound = True
369
        if itemizeFound:
370
            noParagraph = True
371
            if itemize_match:
372
                # Replace the number.
373
                line = re.sub("^[\\*+-]\\s", "\\\\item ", line)
374
                # Look ahead - need empty line to end itemize!
375
                if i < len(text) - 1:
376
                    nextLine = text[i + 1]
377
                    if nextLine == "":
378
                        block.append(line)
379
                        line = "\\end{itemize}"
380
                        itemizeFound = False
381
            else:
382
                # Look ahead - need empty line to end itemize!
383
                if i < len(text) - 1:
384
                    nextLine = text[i + 1]
385
                    if nextLine == "":
386
                        block.append(line)
387
                        line = "\\end{itemize}"
388
                        itemizeFound = False
389
390
        #############################
391
        ## Fix tables.
392
        #############################
393
        # Check if line is part of table.
394
        table_match = re.findall("\\|", line)
395
        if table_match:
396
            if not tableFound:
397
                # Check next line for minimum 3 dashes and the same count of |.
398
                if i < len(text) - 1:
399
                    nextLine = text[i + 1]
400
                    table_match_next = re.findall("\\|", nextLine)
401
                    if table_match_next:
402
                        if len(table_match) == len(table_match_next):
403
                            table_match_dashes = re.findall("-{3,}", nextLine)
404
                            if table_match_dashes:
405
                                tableFound = True
406
                                endPipes = bool(
407
                                    len(table_match) > len(table_match_dashes)
408
                                )
409
                                nextLine = re.sub(":-+:", "c", nextLine)
410
                                nextLine = re.sub("-+:", "r", nextLine)
411
                                nextLine = re.sub("-+", "l", nextLine)
412
                                tableHeader = "\\begin{longtable}{" + nextLine + "}"
413
                                block.append(tableHeader)
414
                                # Fix the header.
415
                                line = re.sub("\\|", "&", line)
416
                                if endPipes:
417
                                    line = re.sub("^\\s*&", "", line)
418
                                    line = re.sub("&\\s*$", "\\\\\\\\", line)
419
                                else:
420
                                    line = line + "\\\\"
421
                            else:
422
                                log.warning(
423
                                    "Possibly incorrectly specified table found."
424
                                )
425
                        else:
426
                            log.warning("Possibly unbalanced table found.")
427
428
            else:
429
                if not headerDone:
430
                    line = "\\hline"
431
                    headerDone = True
432
                else:
433
                    # Fix the line.
434
                    line = re.sub("\\|", "&", line)
435
                    if endPipes:
0 ignored issues
show
introduced by
The variable endPipes does not seem to be defined for all execution paths.
Loading history...
436
                        line = re.sub("^\\s*&", "", line)
437
                        line = re.sub("&\\s*$", "\\\\\\\\", line)
438
                    else:
439
                        line = line + "\\\\"
440
        else:
441
            if tableFound:
442
                block.append("\\end{longtable}")
443
            tableFound = False
444
            headerDone = False
445
        #############################
446
        ## Fix plantuml.
447
        #############################
448
        if plantUMLFound:
449
            noParagraph = True
450
        if re.findall("^plantuml\\s", line):
451
            plantUML_title = re.search('title="(.*)"', line)
452
            if plantUML_title:
453
                plantUMLName = plantUML_title.groups(0)[0]
454
            else:
455
                raise DoorstopError(
456
                    "'title' is required for plantUML processing in LaTeX."
457
                )
458
            plantUMLFile = re.sub("\\s", "-", plantUMLName)
459
            line = "\\begin{plantuml}{" + plantUMLFile + "}"
460
            plantUMLFound = True
461
        if re.findall("@enduml", line):
462
            block.append(line)
463
            block.append("\\end{plantuml}")
464
            line = (
465
                "\\process{" + plantUMLFile + "}{0.8\\textwidth}{" + plantUMLName + "}"
0 ignored issues
show
introduced by
The variable plantUMLName does not seem to be defined for all execution paths.
Loading history...
introduced by
The variable plantUMLFile does not seem to be defined for all execution paths.
Loading history...
466
            )
467
            plantUMLFound = False
468
469
        # Look ahead for empty line and add paragraph.
470
        if i < len(text) - 1:
471
            nextLine = text[i + 1]
472
            if nextLine == "" and not re.search("\\\\", line) and not noParagraph:
473
                line = line + "\\\\"
474
475
        #############################
476
        ## All done. Add the line.
477
        #############################
478
        block.append(line)
479
480
        # Check for end of file and end all environments.
481
        if i == len(text) - 1:
482
            if codeFound:
483
                block.append("\\end{lstlisting}")
484
            if enumerationFound:
485
                block.append("\\end{enumerate}")
486
            if itemizeFound:
487
                block.append("\\end{itemize}")
488
            if plantUMLFound:
489
                block.append("\\end{plantuml}")
490
                block.append(
491
                    "\\process{"
492
                    + plantUMLFile
493
                    + "}{0.8\\textwidth}{"
494
                    + plantUMLName
495
                    + "}"
496
                )
497
            if tableFound:
498
                block.append("\\end{longtable}")
499
    return block
500
501
502
def _matrix_latex(table, path):
503
    """Create a traceability table for LaTeX."""
504
    # Setup.
505
    traceability = []
506
    head, tail = os.path.split(path)
507
    tail = "traceability.tex"
508
    file = os.path.join(head, tail)
509
    count = 0
510
    # Start the table.
511
    table_start = "\\begin{longtable}{"
512
    table_head = ""
513
    header_data = table.__next__()
514
    for column in header_data:
515
        count = count + 1
516
        table_start = table_start + "|l"
517
        if len(table_head) > 0:
518
            table_head = table_head + " & "
519
        table_head = table_head + "\\textbf{" + str(column) + "}"
520
    table_start = table_start + "|}"
521
    table_head = table_head + "\\\\"
522
    traceability.append(table_start)
523
    traceability.append(
524
        "\\caption{Traceability matrix.}\\label{tbl:trace}\\zlabel{tbl:trace}\\\\"
525
    )
526
    traceability.append("\\hline")
527
    traceability.append(table_head)
528
    traceability.append("\\hline")
529
    traceability.append("\\endfirsthead")
530
    traceability.append("\\caption{\\textit{(Continued)} Traceability matrix.}\\\\")
531
    traceability.append("\\hline")
532
    traceability.append(table_head)
533
    traceability.append("\\hline")
534
    traceability.append("\\endhead")
535
    traceability.append("\\hline")
536
    traceability.append(
537
        "\\multicolumn{{{n}}}{{r}}{{\\textit{{Continued on next page.}}}}\\\\".format(
538
            n=count
539
        )
540
    )
541
    traceability.append("\\endfoot")
542
    traceability.append("\\hline")
543
    traceability.append("\\endlastfoot")
544
    # Add rows.
545
    for row in table:
546
        row_text = ""
547
        for column in row:
548
            if len(row_text) > 0:
549
                row_text = row_text + " & "
550
            if column:
551
                row_text = row_text + "\\hyperref[{u}]{{{u}}}".format(u=str(column))
552
            else:
553
                row_text = row_text + " "
554
        row_text = row_text + "\\\\"
555
        traceability.append(row_text)
556
        traceability.append("\\hline")
557
    # End the table.
558
    traceability.append("\\end{longtable}")
559
    common.write_lines(traceability, file)
560