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: |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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
|
|
|
|