Total Complexity | 107 |
Total Lines | 777 |
Duplicated Lines | 7.21 % |
Changes | 0 |
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:
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: |
|
|
|||
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 |