doorstop.core.publishers.latex   F
last analyzed

Complexity

Total Complexity 107

Size/Duplication

Total Lines 777
Duplicated Lines 7.21 %

Importance

Changes 0
Metric Value
wmc 107
eloc 554
dl 56
loc 777
rs 2
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A LaTeXPublisher.format_label_links() 0 6 2
A LaTeXPublisher.__init__() 0 17 1
A LaTeXPublisher._get_compile_path() 0 7 2
A LaTeXPublisher.format_ref() 15 15 3
B LaTeXPublisher.format_references() 31 31 5
B LaTeXPublisher._check_for_eof() 0 24 5
A LaTeXPublisher.preparePublish() 0 4 1
F LaTeXPublisher.lines() 10 108 23
A LaTeXPublisher.publishAction() 0 12 2
B LaTeXPublisher.create_matrix() 0 57 7
A LaTeXPublisher.format_attr_list() 0 8 2
A LaTeXPublisher.create_index() 0 2 1
A LaTeXPublisher.format_links() 0 7 2
F LaTeXPublisher._format_latex_text() 0 211 30
A LaTeXPublisher.format_item_link() 0 8 4
A LaTeXPublisher._typeset_latex_table() 0 16 3
F LaTeXPublisher._generate_latex_wrapper() 0 180 12
A LaTeXPublisher.table_of_contents() 0 2 1
A LaTeXPublisher.concludePublish() 0 10 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like doorstop.core.publishers.latex often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# SPDX-License-Identifier: LGPL-3.0-only
2
3
"""Functions to publish LaTeX documents."""
4
5
import os
6
import re
7
from typing import List
8
9
from doorstop import common, settings
10
from doorstop.cli import utilities
11
from doorstop.common import DoorstopError
12
from doorstop.core.publishers._latex_functions import (
13
    _add_comment,
14
    _check_for_new_table,
15
    _fix_table_line,
16
    _latex_convert,
17
    _typeset_latex_image,
18
)
19
from doorstop.core.publishers.base import (
20
    BasePublisher,
21
    extract_prefix,
22
    get_document_attributes,
23
)
24
from doorstop.core.template import check_latex_template_data, read_template_data
25
from doorstop.core.types import is_item, iter_documents, iter_items
26
27
log = common.logger(__name__)
28
29
30
class LaTeXPublisher(BasePublisher):
31
    """LaTeX publisher."""
32
33
    def __init__(self, obj, ext):
34
        super().__init__(obj, ext)
35
        self.END_LONGTABLE = "\\end{longtable}"
36
        self.HLINE = "\\hline"
37
        self.compile_files = []
38
        self.compile_path = ""
39
        # Define lists.
40
        self.list["start"] = {
41
            "itemize": r"\begin{itemizeDeep}",
42
            "enumerate": r"\begin{enumerateDeep}",
43
        }
44
        self.list["end"] = {
45
            "itemize": r"\end{itemizeDeep}",
46
            "enumerate": r"\end{enumerateDeep}",
47
        }
48
        self.list["start_item"] = {"itemize": r"\\item ", "enumerate": r"\\item "}
49
        self.list["end_item"] = {"itemize": "", "enumerate": ""}
50
51
    def preparePublish(self):
52
        """Publish wrapper files for LaTeX."""
53
        log.debug("Generating compile script for LaTeX from %s", self.path)
54
        self.compile_path = self._get_compile_path()
55
56
    def publishAction(self, document, path):
57
        """Add file to compile.sh script."""
58
        self.document = document
59
        # If path does not end with .tex, add it.
60
        if not path.endswith(".tex"):
61
            self.documentPath = os.path.join(path, document.prefix + ".tex")
62
        else:
63
            self.documentPath = path
64
65
        log.debug("Generating compile script for LaTeX from %s", self.documentPath)
66
        file_to_compile = self._generate_latex_wrapper()
67
        self.compile_files.append(file_to_compile)
68
69
    def concludePublish(self):
70
        """Write out the compile.sh file."""
71
        common.write_lines(
72
            self.compile_files,
73
            self.compile_path,
74
            end=settings.WRITE_LINESEPERATOR,
75
            executable=True,
76
        )
77
        msg = "You can now execute the file 'compile.sh' twice in the exported folder to produce the PDFs!"
78
        utilities.show(msg, flush=True)
79
80
    def create_index(self, directory, index=None, extensions=(".tex",), tree=None):
81
        """No index for LaTeX."""
82
83
    def table_of_contents(self, linkify=None, obj=None):
84
        """No table of contents LaTeX."""
85
86
    def lines(self, obj, **kwargs):
87
        """Yield lines for a LaTeX report.
88
89
        :param obj: Item, list of Items, or Document to publish
90
        :param linkify: turn links into hyperlinks
91
92
        :return: iterator of lines of text
93
94
        """
95
        linkify = kwargs.get("linkify", False)
96
        for item in iter_items(obj):
97
            heading = "\\" + "sub" * (item.depth - 1) + "section*{"
98
            heading_level = "\\" + "sub" * (item.depth - 1) + "section{"
99
100
            if item.heading:
101
                text_lines = item.text.splitlines()
102
                if item.header:
103
                    text_lines.insert(0, item.header)
104
                # Level and Text
105
                if settings.PUBLISH_HEADING_LEVELS:
106
                    standard = "{h}{t}{he}".format(
107
                        h=heading_level,
108
                        t=_latex_convert(text_lines[0]) if text_lines else "",
109
                        he="}",
110
                    )
111
                else:
112
                    standard = "{h}{t}{he}".format(
113
                        h=heading,
114
                        t=_latex_convert(text_lines[0]) if text_lines else "",
115
                        he="}",
116
                    )
117
                attr_list = self.format_attr_list(item, True)
118
                yield standard + attr_list
119
                yield from self._format_latex_text(text_lines[1:])
120
            else:
121
                uid = item.uid
122
                if settings.ENABLE_HEADERS:
123
                    if item.header:
124
                        uid = "{h}{{ - \\small{{}}\\texttt{{}}{u}}}".format(
125
                            h=_latex_convert(item.header), u=item.uid
126
                        )
127
                    else:
128
                        uid = "{u}".format(u=item.uid)
129
130
                # Level and UID
131
                if settings.PUBLISH_BODY_LEVELS:
132
                    standard = "{h}{u}{he}".format(h=heading_level, u=uid, he="}")
133
                else:
134
                    standard = "{h}{u}{he}".format(h=heading, u=uid, he="}")
135
136
                attr_list = self.format_attr_list(item, True)
137
                yield standard + attr_list
138
139
                # Text
140
                if item.text:
141
                    yield ""  # break before text
142
                    yield from self._format_latex_text(item.text.splitlines())
143
144
                # Reference
145
                if item.ref:
146
                    yield ""  # break before reference
147
                    yield self.format_ref(item)
148
149
                # Reference
150
                if item.references:
151
                    yield ""  # break before reference
152
                    yield self.format_references(item)
153
154
                # Parent links
155 View Code Duplication
                if item.links:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
156
                    yield ""  # break before links
157
                    items2 = item.parent_items
158
                    if settings.PUBLISH_CHILD_LINKS:
159
                        label = "Parent links:"
160
                    else:
161
                        label = "Links:"
162
                    links = self.format_links(items2, linkify)
163
                    label_links = self.format_label_links(label, links, linkify)
164
                    yield label_links
165
166
                # Child links
167
                if settings.PUBLISH_CHILD_LINKS:
168
                    items2 = item.find_child_items()
169
                    if items2:
170
                        yield ""  # break before links
171
                        label = "Child links:"
172
                        links = self.format_links(items2, linkify)
173
                        label_links = self.format_label_links(label, links, linkify)
174
                        yield label_links
175
176
                # Add custom publish attributes
177
                if item.document and item.document.publish:
178
                    header_printed = False
179
                    for attr in item.document.publish:
180
                        if not item.attribute(attr):
181
                            continue
182
                        if not header_printed:
183
                            header_printed = True
184
                            yield "\\begin{longtable}{|l|l|}"
185
                            yield "Attribute & Value\\\\"
186
                            yield self.HLINE
187
                        yield "{} & {}".format(attr, item.attribute(attr))
188
                    if header_printed:
189
                        yield self.END_LONGTABLE
190
                    else:
191
                        yield ""
192
193
            yield ""  # break between items
194
195
    def format_attr_list(self, item, linkify):
196
        """Create a LaTeX attribute list for a heading."""
197
        return (
198
            "{l}{u}{le}{zl}{u}{le}".format(
199
                l="\\label{", zl="\\zlabel{", u=item.uid, le="}"
200
            )
201
            if linkify
202
            else ""
203
        )
204
205 View Code Duplication
    def format_ref(self, item):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
206
        """Format an external reference in LaTeX."""
207
        if settings.CHECK_REF:
208
            path, line = item.find_ref()
209
            path = path.replace("\\", "/")  # always use unix-style paths
210
            if line:
211
                return (
212
                    "\\begin{{quote}} \\verb|{p}| (line {line})\\end{{quote}}".format(
213
                        p=path, line=line
214
                    )
215
                )
216
            else:
217
                return "\\begin{{quote}} \\verb|{p}|\\end{{quote}}".format(p=path)
218
        else:
219
            return "\\begin{{quote}} \\verb|{r}|\\end{{quote}}".format(r=item.ref)
220
221 View Code Duplication
    def format_references(self, item):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
222
        """Format an external reference in LaTeX."""
223
        if settings.CHECK_REF:
224
            references = item.find_references()
225
            text_refs = []
226
            for ref_item in references:
227
                path, line = ref_item
228
                path = path.replace("\\", "/")  # always use unix-style paths
229
230
                if line:
231
                    text_refs.append(
232
                        "\\begin{{quote}} \\verb|{p}| (line {line})\\end{{quote}}".format(
233
                            p=path, line=line
234
                        )
235
                    )
236
                else:
237
                    text_refs.append(
238
                        "\\begin{{quote}} \\verb|{p}|\\end{{quote}}".format(p=path)
239
                    )
240
241
            return "\n".join(ref for ref in text_refs)
242
        else:
243
            references = item.references
244
            text_refs = []
245
            for ref_item in references:
246
                path = ref_item["path"]
247
                path = path.replace("\\", "/")  # always use unix-style paths
248
                text_refs.append(
249
                    "\\begin{{quote}} \\verb|{r}|\\end{{quote}}".format(r=path)
250
                )
251
            return "\n".join(ref for ref in text_refs)
252
253
    def format_links(self, items, linkify):
254
        """Format a list of linked items in LaTeX."""
255
        links = []
256
        for item in items:
257
            link = self.format_item_link(item, linkify=linkify)
258
            links.append(link)
259
        return ", ".join(links)
260
261
    def format_item_link(self, item, linkify=True):
262
        """Format an item link in LaTeX."""
263
        if linkify and is_item(item):
264
            if item.header:
265
                return "\\hyperref[{u}]{{{u}}}".format(u=item.uid)
266
            return "\\hyperref[{u}]{{{u}}}".format(u=item.uid)
267
        else:
268
            return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
269
270
    def format_label_links(self, label, links, linkify):
271
        """Join a string of label and links with formatting."""
272
        if linkify:
273
            return "\\textbf{{{lb}}} {ls}".format(lb=label, ls=links)
274
        else:
275
            return "\\textbf{{{lb} {ls}}}".format(lb=label, ls=links)
276
277
    def _typeset_latex_table(
278
        self, table_match, text, i, line, block, table_found, header_done, end_pipes
279
    ):
280
        """Typeset tables."""
281
        if not table_found:
282
            table_found, header_done, line, end_pipes = _check_for_new_table(
283
                table_match, text, i, line, block, table_found, header_done, end_pipes
284
            )
285
        else:
286
            if not header_done:
287
                line = self.HLINE
288
                header_done = True
289
            else:
290
                # Fix the line.
291
                line = _fix_table_line(line, end_pipes)
292
        return table_found, header_done, line, end_pipes
293
294
    def _format_latex_text(self, text):
295
        """Fix all general text formatting to use LaTeX-macros."""
296
        block: List[str]
297
        block = []
298
        environment_data = {}
299
        environment_data["table_found"] = False
300
        header_done = False
301
        environment_data["code_found"] = False
302
        math_found = False
303
        environment_data["plantuml_found"] = False
304
        plantuml_file = ""
305
        plantuml_name = ""
306
        plantuml_count = 0
307
        end_pipes = False
308
        for i, line in enumerate(text):
309
            no_paragraph = False
310
            #############################
311
            ## Fix plantuml.
312
            #############################
313
            if environment_data["plantuml_found"]:
314
                no_paragraph = True
315
            if re.findall("^`*plantuml\\s", line):
316
                plantuml_count = plantuml_count + 1
317
                plantuml_title = re.search('title="(.*)"', line)
318
                if plantuml_title:
319
                    plantuml_name = str(plantuml_title.groups(0)[0])
320
                else:
321
                    raise DoorstopError(
322
                        "'title' is required for plantUML processing in LaTeX."
323
                    )
324
                plantuml_file = re.sub("\\s", "-", plantuml_name)
325
                block.append(
326
                    r"\hyperref[fig:plant"
327
                    + str(plantuml_count)
328
                    + "]{"
329
                    + plantuml_name
330
                    + "}"
331
                )
332
                line = "\\begin{plantuml}{" + plantuml_file + "}"
333
                environment_data["plantuml_found"] = True
334
            if re.findall("@enduml", line):
335
                block.append(line)
336
                block.append("\\end{plantuml}")
337
                line = (
338
                    "\\process{"
339
                    + plantuml_file
340
                    + "}{0.8\\textwidth}{"
341
                    + plantuml_name
342
                    + "}"
343
                    + "{"
344
                    + str(plantuml_count)
345
                    + "}"
346
                )
347
                environment_data["plantuml_found"] = False
348
            # Skip the rest since we are in a plantuml block!
349
            if environment_data["plantuml_found"]:
350
                block.append(line)
351
                # Check for end of file and end all environments.
352
                self._check_for_eof(
353
                    i,
354
                    block,
355
                    text,
356
                    environment_data,
357
                    plantuml_name,
358
                    plantuml_file,
359
                )
360
                continue
361
362
            #############################
363
            ## Fix code blocks.
364
            #############################
365
            code_match = re.findall("```", line)
366
            if environment_data["code_found"]:
367
                no_paragraph = True
368
            if code_match:
369
                # Check previous line of @enduml.
370
                if i > 0:
371
                    previous_line = text[i - 1]
372
                    if re.findall("@enduml", previous_line):
373
                        continue
374
                if environment_data["code_found"]:
375
                    line = "\\end{lstlisting}"
376
                    environment_data["code_found"] = False
377
                else:
378
                    # Check for language.
379
                    language = re.search("```(.*)", line)
380
                    if language and str(language.groups(0)[0]) != "":
381
                        line = (
382
                            "\\begin{lstlisting}[language="
383
                            + str(language.groups(0)[0])
384
                            + "]"
385
                        )
386
                    else:
387
                        line = "\\begin{lstlisting}"
388
                    environment_data["code_found"] = True
389
            # Skip the rest since we are in a code block!
390
            if environment_data["code_found"]:
391
                block.append(line)
392
                # Check for end of file and end all environments.
393
                self._check_for_eof(
394
                    i,
395
                    block,
396
                    text,
397
                    environment_data,
398
                    plantuml_name,
399
                    plantuml_file,
400
                )
401
                continue
402
            # Replace ` for inline code, but not if it is already escaped.
403
            # First replace escaped inline code.
404
            line = re.sub("\\\\`", "##!!TEMPINLINE!!##", line)
405
            # Then replace inline code.
406
            line = re.sub("`(.+?)`", "\\\\lstinline`\\1`", line)
407
            # Then replace escaped inline code back.
408
            line = re.sub("##!!TEMPINLINE!!##", "\\\\`{}", line)
409
410
            #############################
411
            ## Fix images.
412
            #############################
413
            image_match = re.findall(r"!\[(.*)\]\((.*)\)", line)
414
            if image_match:
415
                line = _typeset_latex_image(image_match, line, block)
416
            #############################
417
            ## Fix $ and MATH.
418
            #############################
419
            math_match = re.split("\\$\\$", line)
420
            if len(math_match) > 1:
421
                if math_found and len(math_match) == 2:
422
                    math_found = False
423
                    line = math_match[0] + "$" + _latex_convert(math_match[1])
424
                elif len(math_match) == 2:
425
                    math_found = True
426
                    line = _latex_convert(math_match[0]) + "$" + math_match[1]
427
                elif len(math_match) == 3:
428
                    line = (
429
                        _latex_convert(math_match[0])
430
                        + "$"
431
                        + math_match[1]
432
                        + "$"
433
                        + _latex_convert(math_match[2])
434
                    )
435
                else:
436
                    raise DoorstopError(
437
                        "Cannot handle multiple math environments on one row."
438
                    )
439
            else:
440
                line = _latex_convert(line)
441
            # Skip all other changes if in MATH!
442
            if math_found:
443
                line = line + "\\\\"
444
                block.append(line)
445
                continue
446
            #############################
447
            ## Fix lists.
448
            #############################
449
            # Check if we are at the end of the data.
450
            if i == len(text) - 1:
451
                next_line = ""
452
            else:
453
                next_line = text[i + 1]
454
            (no_paragraph, processed_block, line) = self.process_lists(line, next_line)
455
            if processed_block != "":
456
                block.append(processed_block)
457
            #############################
458
            ## Fix tables.
459
            #############################
460
            # Check if line is part of table.
461
            table_match = re.findall("\\|", line)
462
            if table_match:
463
                (
464
                    environment_data["table_found"],
465
                    header_done,
466
                    line,
467
                    end_pipes,
468
                ) = self._typeset_latex_table(
469
                    table_match,
470
                    text,
471
                    i,
472
                    line,
473
                    block,
474
                    environment_data["table_found"],
475
                    header_done,
476
                    end_pipes,
477
                )
478
            else:
479
                if environment_data["table_found"]:
480
                    block.append(self.END_LONGTABLE)
481
                environment_data["table_found"] = False
482
                header_done = False
483
484
            # Look ahead for empty line and add paragraph.
485
            if i < len(text) - 1:
486
                next_line = text[i + 1]
487
                if next_line == "" and not re.search("\\\\", line) and not no_paragraph:
488
                    line = line + "\\\\"
489
490
            #############################
491
            ## All done. Add the line.
492
            #############################
493
            block.append(line)
494
495
            # Check for end of file and end all environments.
496
            self._check_for_eof(
497
                i,
498
                block,
499
                text,
500
                environment_data,
501
                plantuml_name,
502
                plantuml_file,
503
            )
504
        return block
505
506
    def _check_for_eof(
507
        self,
508
        index,
509
        block,
510
        text,
511
        environment_data,
512
        plantuml_name,
513
        plantuml_file,
514
    ):
515
        """Check for end of file and end all unended environments."""
516
        if index == len(text) - 1:
517
            if environment_data["code_found"]:
518
                block.append("\\end{lstlisting}")
519
            if environment_data["plantuml_found"]:
520
                block.append("\\end{plantuml}")
521
                block.append(
522
                    "\\process{"
523
                    + plantuml_file
524
                    + "}{0.8\\textwidth}{"
525
                    + plantuml_name
526
                    + "}"
527
                )
528
            if environment_data["table_found"]:
529
                block.append(self.END_LONGTABLE)
530
531
    def create_matrix(self, directory):
532
        """Create a traceability table for LaTeX."""
533
        # Setup.
534
        table = self.object.get_traceability().__iter__()
535
        traceability = []
536
        file = os.path.join(directory, "traceability.tex")
537
        count = 0
538
        # Start the table.
539
        table_start = "\\begin{longtable}{"
540
        table_head = ""
541
        header_data = table.__next__()
542
        for column in header_data:
543
            count = count + 1
544
            table_start = table_start + "|l"
545
            if len(table_head) > 0:
546
                table_head = table_head + " & "
547
            table_head = table_head + "\\textbf{" + str(column) + "}"
548
        table_start = table_start + "|}"
549
        table_head = table_head + "\\\\"
550
        traceability.append(table_start)
551
        traceability.append(
552
            "\\caption{Traceability matrix.}\\label{tbl:trace}\\zlabel{tbl:trace}\\\\"
553
        )
554
        traceability.append(self.HLINE)
555
        traceability.append(table_head)
556
        traceability.append(self.HLINE)
557
        traceability.append("\\endfirsthead")
558
        traceability.append("\\caption{\\textit{(Continued)} Traceability matrix.}\\\\")
559
        traceability.append(self.HLINE)
560
        traceability.append(table_head)
561
        traceability.append(self.HLINE)
562
        traceability.append("\\endhead")
563
        traceability.append(self.HLINE)
564
        traceability.append(
565
            "\\multicolumn{{{n}}}{{r}}{{\\textit{{Continued on next page.}}}}\\\\".format(
566
                n=count
567
            )
568
        )
569
        traceability.append("\\endfoot")
570
        traceability.append(self.HLINE)
571
        traceability.append("\\endlastfoot")
572
        # Add rows.
573
        for row in table:
574
            row_text = ""
575
            for column in row:
576
                if len(row_text) > 0:
577
                    row_text = row_text + " & "
578
                if column:
579
                    row_text = row_text + "\\hyperref[{u}]{{{u}}}".format(u=str(column))
580
                else:
581
                    row_text = row_text + " "
582
            row_text = row_text + "\\\\"
583
            traceability.append(row_text)
584
            traceability.append(self.HLINE)
585
        # End the table.
586
        traceability.append(self.END_LONGTABLE)
587
        common.write_lines(traceability, file, end=settings.WRITE_LINESEPERATOR)
588
589
    def _get_compile_path(self):
590
        """Return the path to the compile script."""
591
        head, tail = os.path.split(self.path)
592
        # If tail ends with .tex, replace it with compile.sh.
593
        if tail.endswith(".tex"):
594
            return os.path.join(head, "compile.sh")
595
        return os.path.join(self.path, "compile.sh")
596
597
    def _generate_latex_wrapper(self):
598
        """Generate all wrapper scripts required for typesetting in LaTeX."""
599
        # Check for defined document attributes.
600
        doc_attributes = get_document_attributes(self.document)
601
        # Create the wrapper file.
602
        head, tail = os.path.split(self.documentPath)
603
        if tail != extract_prefix(self.document) + ".tex":
604
            log.warning(
605
                "LaTeX export does not support custom file names. Change in .doorstop.yml instead."
606
            )
607
        tail = doc_attributes["name"] + ".tex"
608
        self.documentPath = os.path.join(head, extract_prefix(self.document) + ".tex")
609
        wrapperPath = os.path.join(head, tail)
610
        # Load template data.
611
        templatePath = os.path.abspath(os.path.join(self.assetsPath, "..", "template"))
612
        log.info(
613
            "Loading template data from {}/{}.yml".format(templatePath, self.template)
614
        )
615
        template_data = read_template_data(self.assetsPath, self.template)
616
        check_latex_template_data(
617
            template_data, "{}/{}.yml".format(templatePath, self.template)
618
        )
619
        wrapper = []
620
        wrapper.append(
621
            "\\documentclass[%s]{template/%s}"
622
            % (", ".join(template_data["documentclass"]), self.template)
623
        )
624
        # Add required packages.
625
        wrapper = _add_comment(
626
            wrapper,
627
            "These packages are required.",
628
        )
629
        wrapper.append("\\usepackage{enumitem}")
630
        wrapper = _add_comment(wrapper, "END required packages.")
631
        wrapper.append("")
632
633
        # Add required packages from template data.
634
        wrapper = _add_comment(
635
            wrapper,
636
            "These packages were automatically added from the template configuration file.",
637
        )
638
        for package, options in template_data["usepackage"].items():
639
            package_line = "\\usepackage"
640
            if options:
641
                package_line += "[%s]" % ", ".join(options)
642
            package_line += "{%s}" % package
643
            wrapper.append(package_line)
644
        wrapper = _add_comment(
645
            wrapper, "END data from the template configuration file."
646
        )
647
        wrapper.append("")
648
        wrapper = _add_comment(
649
            wrapper,
650
            "These fields are generated from the default doc attribute in the .doorstop.yml file.",
651
        )
652
        wrapper.append(
653
            "\\def\\doccopyright{{{n}}}".format(
654
                n=_latex_convert(doc_attributes["copyright"])
655
            )
656
        )
657
        wrapper.append(
658
            "\\def\\doccategory{{{t}}}".format(
659
                t=_latex_convert(extract_prefix(self.document))
660
            )
661
        )
662
        wrapper.append(
663
            "\\def\\doctitle{{{n}}}".format(n=_latex_convert(doc_attributes["title"]))
664
        )
665
        wrapper.append(
666
            "\\def\\docref{{{n}}}".format(n=_latex_convert(doc_attributes["ref"]))
667
        )
668
        wrapper.append(
669
            "\\def\\docby{{{n}}}".format(n=_latex_convert(doc_attributes["by"]))
670
        )
671
        wrapper.append(
672
            "\\def\\docissuemajor{{{n}}}".format(
673
                n=_latex_convert(doc_attributes["major"])
674
            )
675
        )
676
        wrapper.append(
677
            "\\def\\docissueminor{{{n}}}".format(
678
                n=_latex_convert(doc_attributes["minor"])
679
            )
680
        )
681
        wrapper = _add_comment(wrapper, "END data from the .doorstop.yml file.")
682
        wrapper.append("")
683
684
        wrapper = _add_comment(
685
            wrapper,
686
            "LaTex is limited to four (4) levels of lists. The following code extends this to nine (9) levels.",
687
        )
688
        wrapper.append("% ******************************************************")
689
        wrapper.append("% Increase nesting level for lists")
690
        wrapper.append("% ******************************************************")
691
        wrapper.append("\\setlistdepth{9}")
692
        wrapper.append("\\newlist{itemizeDeep}{enumerate}{9}")
693
        wrapper.append("\\setlist[itemizeDeep,1]{label=\\textbullet}")
694
        wrapper.append(
695
            "\\setlist[itemizeDeep,2]{label=\\normalfont\\bfseries \\textendash}"
696
        )
697
        wrapper.append("\\setlist[itemizeDeep,3]{label=\\textasteriskcentered}")
698
        wrapper.append("\\setlist[itemizeDeep,4]{label=\\textperiodcentered}")
699
        wrapper.append("\\setlist[itemizeDeep,5]{label=\\textopenbullet}")
700
        wrapper.append("\\setlist[itemizeDeep,6]{label=\\textbullet}")
701
        wrapper.append(
702
            "\\setlist[itemizeDeep,7]{label=\\normalfont\\bfseries \\textendash}"
703
        )
704
        wrapper.append("\\setlist[itemizeDeep,8]{label=\\textasteriskcentered}")
705
        wrapper.append("\\setlist[itemizeDeep,9]{label=\\textperiodcentered}")
706
        wrapper.append("\\newlist{enumerateDeep}{enumerate}{9}")
707
        wrapper.append("\\setlist[enumerateDeep]{label*=\\arabic*.}")
708
        wrapper = _add_comment(wrapper, "END list depth fix.")
709
        wrapper.append("")
710
711
        info_text_set = False
712
        for external, _ in iter_documents(self.object, self.path, ".tex"):
713
            # Check for defined document attributes.
714
            external_doc_attributes = get_document_attributes(external)
715
            # Don't add self.
716
            if external_doc_attributes["name"] != doc_attributes["name"]:
717
                if not info_text_set:
718
                    wrapper = _add_comment(
719
                        wrapper,
720
                        "These are automatically added external references to make cross-references work between the PDFs.",
721
                    )
722
                    info_text_set = True
723
                wrapper.append(
724
                    "\\zexternaldocument{{{n}}}".format(
725
                        n=external_doc_attributes["name"]
726
                    )
727
                )
728
                wrapper.append(
729
                    "\\externaldocument{{{n}}}".format(
730
                        n=external_doc_attributes["name"]
731
                    )
732
                )
733
        if info_text_set:
734
            wrapper = _add_comment(wrapper, "END external references.")
735
            wrapper.append("")
736
        wrapper = _add_comment(
737
            wrapper,
738
            "These lines were automatically added from the template configuration file to allow full customization of the template _before_ \\begin{document}.",
739
        )
740
        for line in template_data["before_begin_document"]:
741
            wrapper.append(line)
742
        wrapper = _add_comment(
743
            wrapper, "END custom data from the template configuration file."
744
        )
745
        wrapper.append("")
746
        wrapper.append("\\begin{document}")
747
        wrapper = _add_comment(
748
            wrapper,
749
            "These lines were automatically added from the template configuration file to allow full customization of the template _after_ \\begin{document}.",
750
        )
751
        for line in template_data["after_begin_document"]:
752
            wrapper.append(line)
753
        wrapper = _add_comment(
754
            wrapper, "END custom data from the template configuration file."
755
        )
756
        wrapper.append("")
757
        wrapper = _add_comment(wrapper, "Load the doorstop data file.")
758
        wrapper.append("\\input{{{n}.tex}}".format(n=extract_prefix(self.document)))
759
        wrapper = _add_comment(wrapper, "END doorstop data file.")
760
        wrapper.append("")
761
        # Include traceability matrix
762
        if self.matrix:
763
            wrapper = _add_comment(wrapper, "Add traceability matrix.")
764
            if settings.PUBLISH_HEADING_LEVELS:
765
                wrapper.append("\\section{Traceability}")
766
            else:
767
                wrapper.append("\\section*{Traceability}")
768
            wrapper.append("\\input{traceability.tex}")
769
            wrapper = _add_comment(wrapper, "END traceability matrix.")
770
            wrapper.append("")
771
        wrapper.append("\\end{document}")
772
        common.write_lines(wrapper, wrapperPath, end=settings.WRITE_LINESEPERATOR)
773
774
        # Add to compile.sh as return value.
775
        return "pdflatex -halt-on-error -shell-escape {n}.tex".format(
776
            n=doc_attributes["name"]
777
        )
778