Completed
Push — develop ( 0f6046...f40dba )
by Jace
15s queued 13s
created

_get_document_attributes()   C

Complexity

Conditions 10

Size

Total Lines 31
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 31
rs 5.9999
c 0
b 0
f 0
cc 10
nop 1

How to fix   Complexity   

Complexity

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