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

doorstop/core/publisher.py (1 issue)

1
# SPDX-License-Identifier: LGPL-3.0-only
2
3
"""Functions to publish documents and items."""
4
5
import os
6
import tempfile
7
import textwrap
8
9
import bottle
10
import markdown
11
from bottle import template as bottle_template
12
from plantuml_markdown import PlantUMLMarkdownExtension
13
14
from doorstop import common, settings
15
from doorstop.cli import utilities
16
from doorstop.common import DoorstopError
17
from doorstop.core import Document
18
from doorstop.core.publisher_latex import _lines_latex, _matrix_latex
19
from doorstop.core.types import is_item, is_tree, iter_documents, iter_items
20
21
EXTENSIONS = (
22
    "markdown.extensions.extra",
23
    "markdown.extensions.sane_lists",
24
    PlantUMLMarkdownExtension(
25
        server="http://www.plantuml.com/plantuml",
26
        cachedir=tempfile.gettempdir(),
27
        format="svg",
28
        classes="class1,class2",
29
        title="UML",
30
        alt="UML Diagram",
31
    ),
32
)
33
CSS = os.path.join(os.path.dirname(__file__), "files", "doorstop.css")
34
HTMLTEMPLATE = "sidebar"
35
INDEX = "index.html"
36
MATRIX = "traceability.csv"
37
38
log = common.logger(__name__)
39
40
41
def publish(
42
    obj,
43
    path,
44
    ext=None,
45
    linkify=None,
46
    index=None,
47
    matrix=None,
48
    template=None,
49
    toc=True,
50
    **kwargs,
51
):
52
    """Publish an object to a given format.
53
54
    The function can be called in two ways:
55
56
    1. document or item-like object + output file path
57
    2. tree-like object + output directory path
58
59
    :param obj: (1) Item, list of Items, Document or (2) Tree
60
    :param path: (1) output file path or (2) output directory path
61
    :param ext: file extension to override output extension
62
    :param linkify: turn links into hyperlinks (for Markdown or HTML)
63
    :param index: create an index.html (for HTML)
64
    :param matrix: create a traceability matrix, traceability.csv
65
66
    :raises: :class:`doorstop.common.DoorstopError` for unknown file formats
67
68
    :return: output location if files created, else None
69
70
    """
71
    # Determine the output format
72
    ext = ext or os.path.splitext(path)[-1] or ".html"
73
    check(ext)
74
    if linkify is None:
75
        linkify = is_tree(obj) and ext in [".html", ".md", ".tex"]
76
    if index is None:
77
        index = is_tree(obj) and ext == ".html"
78
    if matrix is None:
79
        matrix = is_tree(obj)
80
81
    if is_tree(obj):
82
        assets_dir = os.path.join(path, Document.ASSETS)  # path is a directory name
83
    else:
84
        assets_dir = os.path.join(
85
            os.path.dirname(path), Document.ASSETS
86
        )  # path is a filename
87
88
    if os.path.isdir(assets_dir):
89
        log.info("Deleting contents of assets directory %s", assets_dir)
90
        common.delete_contents(assets_dir)
91
    else:
92
        os.makedirs(assets_dir)
93
94
    # If publish html and then markdown ensure that the html template are still available
95
    if not template:
96
        template = HTMLTEMPLATE
97
    template_assets = os.path.join(os.path.dirname(__file__), "files", "assets")
98
    if os.path.isdir(template_assets):
99
        if ext == ".tex":
100
            template_assets = template_assets + "/latex"
101
        log.info("Copying %s to %s", template_assets, assets_dir)
102
        common.copy_dir_contents(template_assets, assets_dir)
103
104
    # Publish documents
105
    count = 0
106
    compile_files = []
107
    compile_path = ""
108
    for obj2, path2 in iter_documents(obj, path, ext):
109
        count += 1
110
        # Publish wrapper files for LaTeX.
111
        if ext == ".tex":
112
            head, tail = os.path.split(path2)
113
            tail = "compile.sh"
114
            compile_path = os.path.join(head, tail)
115
            # Check for defined document attributes.
116
            document_name = "doc-" + str(obj2)
117
            document_title = "Test document for development of \\textit{Doorstop}"
118
            document_ref = ""
119
            document_by = ""
120
            document_major = ""
121
            document_minor = ""
122
            try:
123
                attribute_defaults = obj2.__getattribute__("_attribute_defaults")
124
                if attribute_defaults:
125
                    if attribute_defaults["doc"]["name"]:
126
                        document_name = attribute_defaults["doc"]["name"]
127
                    if attribute_defaults["doc"]["title"]:
128
                        document_title = attribute_defaults["doc"]["title"]
129
                    if attribute_defaults["doc"]["ref"]:
130
                        document_ref = attribute_defaults["doc"]["ref"]
131
                    if attribute_defaults["doc"]["by"]:
132
                        document_by = attribute_defaults["doc"]["by"]
133
                    if attribute_defaults["doc"]["major"]:
134
                        document_major = attribute_defaults["doc"]["major"]
135
                    if attribute_defaults["doc"]["minor"]:
136
                        document_minor = attribute_defaults["doc"]["minor"]
137
            except AttributeError:
138
                pass
139
            # Add to compile.sh
140
            compile_files.append(
141
                "pdflatex -shell-escape {n}.tex".format(n=document_name)
142
            )
143
            # Create the wrapper file.
144
            head, tail = os.path.split(path2)
145
            if tail != str(obj2) + ".tex":
146
                log.warning(
147
                    "LaTeX export does not support custom file names. Change in .doorstop.yml instead."
148
                )
149
            tail = document_name + ".tex"
150
            path2 = os.path.join(head, str(obj2) + ".tex")
151
            path3 = os.path.join(head, tail)
152
            wrapper = []
153
            wrapper.append("\\documentclass[a4paper, twoside]{assets/doorstop}")
154
            wrapper.append("\\usepackage[utf8]{inputenc}")
155
            wrapper.append("")
156
            wrapper.append("%% Change these to change logotype and/or copyright.")
157
            wrapper.append("% \\def\\owner{Whatever Inc.}")
158
            wrapper.append("% \\def\\logo{assets/logo-black-white.png}")
159
            wrapper.append("% \\definetrim{logotrim}{0 100px 0 100px}")
160
            wrapper.append("")
161
            wrapper.append("% Define the header.")
162
            wrapper.append("\\def\\doccategory{{{t}}}".format(t=str(obj2)))
163
            wrapper.append("\\def\\docname{Doorstop - \\doccategory{}}")
164
            wrapper.append("\\def\\doctitle{{{n}}}".format(n=document_title))
165
            wrapper.append("\\def\\docref{{{n}}}".format(n=document_ref))
166
            wrapper.append("\\def\\docby{{{n}}}".format(n=document_by))
167
            wrapper.append("\\def\\docissuemajor{{{n}}}".format(n=document_major))
168
            wrapper.append("\\def\\docissueminor{{{n}}}".format(n=document_minor))
169
            wrapper.append("")
170
            info_text_set = False
171
            for external, _ in iter_documents(obj, path, ext):
172
                # Check for defined document attributes.
173
                external_doc_name = "doc-" + str(external)
174
                try:
175
                    external_attribute_defaults = external.__getattribute__(
176
                        "_attribute_defaults"
177
                    )
178
                    if external_attribute_defaults:
179
                        if external_attribute_defaults["doc"]["name"]:
180
                            external_doc_name = external_attribute_defaults["doc"][
181
                                "name"
182
                            ]
183
                except AttributeError:
184
                    pass
185
                # Don't add self.
186
                if external_doc_name != document_name:
187
                    if not info_text_set:
188
                        wrapper.append(
189
                            "% Add all documents as external references to allow cross-references."
190
                        )
191
                        info_text_set = True
192
                    wrapper.append(
193
                        "\\zexternaldocument{{{n}}}".format(n=external_doc_name)
194
                    )
195
                    wrapper.append(
196
                        "\\externaldocument{{{n}}}".format(n=external_doc_name)
197
                    )
198
            wrapper.append("")
199
            wrapper.append("\\begin{document}")
200
            wrapper.append("\\makecoverpage")
201
            wrapper.append("\\maketoc")
202
            wrapper.append("% Load the output file.")
203
            wrapper.append("\\input{{{n}.tex}}".format(n=str(obj2)))
204
            # Include traceability matrix
205
            if matrix and count:
206
                wrapper.append("% Traceability matrix")
207
                if settings.PUBLISH_HEADING_LEVELS:
208
                    wrapper.append("\\section{Traceability}")
209
                else:
210
                    wrapper.append("\\section*{Traceability}")
211
                wrapper.append("\\input{traceability.tex}")
212
            wrapper.append("\\end{document}")
213
            common.write_lines(wrapper, path3)
214
215
        # Publish content to the specified path
216
        log.info("publishing to {}...".format(path2))
217
        lines = publish_lines(
218
            obj2, ext, linkify=linkify, template=template, toc=toc, **kwargs
219
        )
220
        common.write_lines(lines, path2)
221
        if obj2.copy_assets(assets_dir):
222
            log.info("Copied assets from %s to %s", obj.assets, assets_dir)
223
224
    if ext == ".tex":
225
        common.write_lines(compile_files, compile_path)
226
        msg = "You can now execute the file 'compile.sh' twice in the exported folder to produce the PDFs!"
227
        utilities.show(msg, flush=True)
228
229
    # Create index
230
    if index and count:
231
        _index(path, tree=obj if is_tree(obj) else None)
232
233
    # Create traceability matrix
234
    if (index or ext == ".tex") and (matrix and count):
235
        _matrix(
236
            path, tree=obj if is_tree(obj) else None, ext=ext if ext == ".tex" else None
237
        )
238
239
    # Return the published path
240
    if count:
241
        msg = "published to {} file{}".format(count, "s" if count > 1 else "")
242
        log.info(msg)
243
        return path
244
    else:
245
        log.warning("nothing to publish")
246
        return None
247
248
249
def _index(directory, index=INDEX, extensions=(".html",), tree=None):
250
    """Create an HTML index of all files in a directory.
251
252
    :param directory: directory for index
253
    :param index: filename for index
254
    :param extensions: file extensions to include
255
    :param tree: optional tree to determine index structure
256
257
    """
258
    # Get paths for the index index
259
    filenames = []
260
    for filename in os.listdir(directory):
261
        if filename.endswith(extensions) and filename != INDEX:
262
            filenames.append(os.path.join(filename))
263
264
    # Create the index
265
    if filenames:
266
        path = os.path.join(directory, index)
267
        log.info("creating an {}...".format(index))
268
        lines = _lines_index(sorted(filenames), tree=tree)
269
        common.write_lines(lines, path)
270
    else:
271
        log.warning("no files for {}".format(index))
272
273
274
def _lines_index(filenames, charset="UTF-8", tree=None):
275
    """Yield lines of HTML for index.html.
276
277
    :param filesnames: list of filenames to add to the index
278
    :param charset: character encoding for output
279
    :param tree: optional tree to determine index structure
280
281
    """
282
    yield "<!DOCTYPE html>"
283
    yield "<head>"
284
    yield (
285
        '<meta http-equiv="content-type" content="text/html; '
286
        'charset={charset}">'.format(charset=charset)
287
    )
288
    yield '<style type="text/css">'
289
    yield from _lines_css()
290
    yield "</style>"
291
    yield "</head>"
292
    yield "<body>"
293
    # Tree structure
294
    text = tree.draw() if tree else None
295
    if text:
296
        yield ""
297
        yield "<h3>Tree Structure:</h3>"
298
        yield "<pre><code>" + text + "</pre></code>"
299
    # Additional files
300
    if filenames:
301
        if text:
302
            yield ""
303
            yield "<hr>"
304
        yield ""
305
        yield "<h3>Published Documents:</h3>"
306
        yield "<p>"
307
        yield "<ul>"
308
        for filename in filenames:
309
            name = os.path.splitext(filename)[0]
310
            yield '<li> <a href="{f}">{n}</a> </li>'.format(f=filename, n=name)
311
        yield "</ul>"
312
        yield "</p>"
313
    # Traceability table
314
    documents = tree.documents if tree else None
315
    if documents:
316
        if text or filenames:
317
            yield ""
318
            yield "<hr>"
319
        yield ""
320
        # table
321
        yield "<h3>Item Traceability:</h3>"
322
        yield "<p>"
323
        yield "<table>"
324
        # header
325
        for document in documents:  # pylint: disable=not-an-iterable
326
            yield '<col width="100">'
327
        yield "<tr>"
328
        for document in documents:  # pylint: disable=not-an-iterable
329
            link = '<a href="{p}.html">{p}</a>'.format(p=document.prefix)
330
            yield ('  <th height="25" align="center"> {link} </th>'.format(link=link))
331
        yield "</tr>"
332
        # data
333
        for index, row in enumerate(tree.get_traceability()):
334
            if index % 2:
335
                yield '<tr class="alt">'
336
            else:
337
                yield "<tr>"
338
            for item in row:
339
                if item is None:
340
                    link = ""
341
                else:
342
                    link = _format_html_item_link(item)
343
                yield '  <td height="25" align="center"> {} </td>'.format(link)
344
            yield "</tr>"
345
        yield "</table>"
346
        yield "</p>"
347
    yield ""
348
    yield "</body>"
349
    yield "</html>"
350
351
352
def _lines_css():
353
    """Yield lines of CSS to embedded in HTML."""
354
    yield ""
355
    for line in common.read_lines(CSS):
356
        yield line.rstrip()
357
    yield ""
358
359
360
def _matrix(directory, tree, filename=MATRIX, ext=None):
361
    """Create a traceability matrix for all the items.
362
363
    :param directory: directory for matrix
364
    :param tree: tree to access the traceability data
365
    :param filename: filename for matrix
366
    :param ext: file extensionto use for the matrix
367
368
    """
369
    # Get path and format extension
370
    path = os.path.join(directory, filename)
371
    ext = ext or os.path.splitext(path)[-1] or ".csv"
372
373
    # Create the matrix
374
    if tree:
375
        log.info("creating an {}...".format(filename))
376
        content = _matrix_content(tree)
377
        if ext == ".tex":
378
            _matrix_latex(content, path)
379
        else:
380
            common.write_csv(content, path)
381
    else:
382
        log.warning("no data for {}".format(filename))
383
384
385
def _extract_prefix(document):
386
    if document:
387
        return document.prefix
388
    else:
389
        return None
390
391
392
def _extract_uid(item):
393
    if item:
394
        return item.uid
395
    else:
396
        return None
397
398
399
def _matrix_content(tree):
400
    """Yield rows of content for the traceability matrix."""
401
    yield tuple(map(_extract_prefix, tree.documents))
402
    for row in tree.get_traceability():
403
        yield tuple(map(_extract_uid, row))
404
405
406
def publish_lines(obj, ext=".txt", **kwargs):
407
    """Yield lines for a report in the specified format.
408
409
    :param obj: Item, list of Items, or Document to publish
410
    :param ext: file extension to specify the output format
411
412
    :raises: :class:`doorstop.common.DoorstopError` for unknown file formats
413
414
    """
415
    gen = check(ext)
416
    log.debug("yielding {} as lines of {}...".format(obj, ext))
417
    yield from gen(obj, **kwargs)
418
419
420
def _lines_text(obj, indent=8, width=79, **_):
421
    """Yield lines for a text report.
422
423
    :param obj: Item, list of Items, or Document to publish
424
    :param indent: number of spaces to indent text
425
    :param width: maximum line length
426
427
    :return: iterator of lines of text
428
429
    """
430
    for item in iter_items(obj):
431
432
        level = _format_level(item.level)
433
434
        if item.heading:
435
436
            # Level and Text
437
            if settings.PUBLISH_HEADING_LEVELS:
438
                yield "{lev:<{s}}{t}".format(lev=level, s=indent, t=item.text)
439
            else:
440
                yield "{t}".format(t=item.text)
441
442
        else:
443
444
            # Level and UID
445
            if item.header:
446
                yield "{lev:<{s}}{u} {header}".format(
447
                    lev=level, s=indent, u=item.uid, header=item.header
448
                )
449
            else:
450
                yield "{lev:<{s}}{u}".format(lev=level, s=indent, u=item.uid)
451
452
            # Text
453
            if item.text:
454
                yield ""  # break before text
455
                for line in item.text.splitlines():
456
                    yield from _chunks(line, width, indent)
457
458
                    if not line:
459
                        yield ""  # break between paragraphs
460
461
            # Reference
462
            if item.ref:
463
                yield ""  # break before reference
464
                ref = _format_text_ref(item)
465
                yield from _chunks(ref, width, indent)
466
467
            # References
468
            if item.references:
469
                yield ""  # break before references
470
                ref = _format_text_references(item)
471
                yield from _chunks(ref, width, indent)
472
473
            # Links
474
            if item.links:
475
                yield ""  # break before links
476
                if settings.PUBLISH_CHILD_LINKS:
477
                    label = "Parent links: "
478
                else:
479
                    label = "Links: "
480
                slinks = label + ", ".join(str(l) for l in item.links)
481
                yield from _chunks(slinks, width, indent)
482
            if settings.PUBLISH_CHILD_LINKS:
483
                links = item.find_child_links()
484
                if links:
485
                    yield ""  # break before links
486
                    slinks = "Child links: " + ", ".join(str(l) for l in links)
487
                    yield from _chunks(slinks, width, indent)
488
489
            if item.document and item.document.publish:
490
                yield ""
491
                for attr in item.document.publish:
492
                    if not item.attribute(attr):
493
                        continue
494
                    attr_line = "{}: {}".format(attr, item.attribute(attr))
495
                    yield from _chunks(attr_line, width, indent)
496
497
        yield ""  # break between items
498
499
500
def _chunks(text, width, indent):
501
    """Yield wrapped lines of text."""
502
    yield from textwrap.wrap(
503
        text, width, initial_indent=" " * indent, subsequent_indent=" " * indent
504
    )
505
506
507
def _lines_markdown(obj, **kwargs):
508
    """Yield lines for a Markdown report.
509
510
    :param obj: Item, list of Items, or Document to publish
511
    :param linkify: turn links into hyperlinks (for conversion to HTML)
512
513
    :return: iterator of lines of text
514
515
    """
516
    linkify = kwargs.get("linkify", False)
517
    for item in iter_items(obj):
518
519
        heading = "#" * item.depth
520
        level = _format_level(item.level)
521
522
        if item.heading:
523
            text_lines = item.text.splitlines()
524
            # Level and Text
525
            if settings.PUBLISH_HEADING_LEVELS:
526
                standard = "{h} {lev} {t}".format(
527
                    h=heading, lev=level, t=text_lines[0] if text_lines else ""
528
                )
529
            else:
530
                standard = "{h} {t}".format(
531
                    h=heading, t=text_lines[0] if text_lines else ""
532
                )
533
            attr_list = _format_md_attr_list(item, True)
534
            yield standard + attr_list
535
            yield from text_lines[1:]
536
        else:
537
538
            uid = item.uid
539
            if settings.ENABLE_HEADERS:
540
                if item.header:
541
                    uid = "{h} <small>{u}</small>".format(h=item.header, u=item.uid)
542
                else:
543
                    uid = "{u}".format(u=item.uid)
544
545
            # Level and UID
546
            if settings.PUBLISH_BODY_LEVELS:
547
                standard = "{h} {lev} {u}".format(h=heading, lev=level, u=uid)
548
            else:
549
                standard = "{h} {u}".format(h=heading, u=uid)
550
551
            attr_list = _format_md_attr_list(item, True)
552
            yield standard + attr_list
553
554
            # Text
555
            if item.text:
556
                yield ""  # break before text
557
                yield from item.text.splitlines()
558
559
            # Reference
560
            if item.ref:
561
                yield ""  # break before reference
562
                yield _format_md_ref(item)
563
564
            # Reference
565
            if item.references:
566
                yield ""  # break before reference
567
                yield _format_md_references(item)
568
569
            # Parent links
570
            if item.links:
571
                yield ""  # break before links
572
                items2 = item.parent_items
573
                if settings.PUBLISH_CHILD_LINKS:
574
                    label = "Parent links:"
575
                else:
576
                    label = "Links:"
577
                links = _format_md_links(items2, linkify)
578
                label_links = _format_md_label_links(label, links, linkify)
579
                yield label_links
580
581
            # Child links
582
            if settings.PUBLISH_CHILD_LINKS:
583
                items2 = item.find_child_items()
584
                if items2:
585
                    yield ""  # break before links
586
                    label = "Child links:"
587
                    links = _format_md_links(items2, linkify)
588
                    label_links = _format_md_label_links(label, links, linkify)
589
                    yield label_links
590
591
            # Add custom publish attributes
592 View Code Duplication
            if item.document and item.document.publish:
593
                header_printed = False
594
                for attr in item.document.publish:
595
                    if not item.attribute(attr):
596
                        continue
597
                    if not header_printed:
598
                        header_printed = True
599
                        yield ""
600
                        yield "| Attribute | Value |"
601
                        yield "| --------- | ----- |"
602
                    yield "| {} | {} |".format(attr, item.attribute(attr))
603
                yield ""
604
605
        yield ""  # break between items
606
607
608
def _format_level(level):
609
    """Convert a level to a string and keep zeros if not a top level."""
610
    text = str(level)
611
    if text.endswith(".0") and len(text) > 3:
612
        text = text[:-2]
613
    return text
614
615
616
def _format_md_attr_list(item, linkify):
617
    """Create a Markdown attribute list for a heading."""
618
    return " {{#{u} }}".format(u=item.uid) if linkify else ""
619
620
621
def _format_text_ref(item):
622
    """Format an external reference in text."""
623
    if settings.CHECK_REF:
624
        path, line = item.find_ref()
625
        path = path.replace("\\", "/")  # always use unix-style paths
626
        if line:
627
            return "Reference: {p} (line {line})".format(p=path, line=line)
628
        else:
629
            return "Reference: {p}".format(p=path)
630
    else:
631
        return "Reference: '{r}'".format(r=item.ref)
632
633
634 View Code Duplication
def _format_text_references(item):
635
    """Format an external reference in text."""
636
    if settings.CHECK_REF:
637
        ref = item.find_references()
638
        text_refs = []
639
        for ref_item in ref:
640
            path, line = ref_item
641
            path = path.replace("\\", "/")  # always use unix-style paths
642
            if line:
643
                text_refs.append("{p} (line {line})".format(p=path, line=line))
644
            else:
645
                text_refs.append("{p}".format(p=path))
646
        return "Reference: {}".format(", ".join(ref for ref in text_refs))
647
    else:
648
        references = item.references
649
        text_refs = []
650
        for ref_item in references:
651
            path = ref_item["path"]
652
            path = path.replace("\\", "/")  # always use unix-style paths
653
            text_refs.append("'{p}'".format(p=path))
654
        return "Reference: {}".format(", ".join(text_ref for text_ref in text_refs))
655
656
657
def _format_md_ref(item):
658
    """Format an external reference in Markdown."""
659
    if settings.CHECK_REF:
660
        path, line = item.find_ref()
661
        path = path.replace("\\", "/")  # always use unix-style paths
662
        if line:
663
            return "> `{p}` (line {line})".format(p=path, line=line)
664
        else:
665
            return "> `{p}`".format(p=path)
666
    else:
667
        return "> '{r}'".format(r=item.ref)
668
669
670 View Code Duplication
def _format_md_references(item):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
671
    """Format an external reference in Markdown."""
672
    if settings.CHECK_REF:
673
        references = item.find_references()
674
        text_refs = []
675
        for ref_item in references:
676
            path, line = ref_item
677
            path = path.replace("\\", "/")  # always use unix-style paths
678
679
            if line:
680
                text_refs.append("> `{p}` (line {line})".format(p=path, line=line))
681
            else:
682
                text_refs.append("> `{p}`".format(p=path))
683
684
        return "\n".join(ref for ref in text_refs)
685
    else:
686
        references = item.references
687
        text_refs = []
688
        for ref_item in references:
689
            path = ref_item["path"]
690
            path = path.replace("\\", "/")  # always use unix-style paths
691
            text_refs.append("> '{r}'".format(r=path))
692
        return "\n".join(ref for ref in text_refs)
693
694
695
def _format_md_links(items, linkify):
696
    """Format a list of linked items in Markdown."""
697
    links = []
698
    for item in items:
699
        link = _format_md_item_link(item, linkify=linkify)
700
        links.append(link)
701
    return ", ".join(links)
702
703
704
def _format_md_item_link(item, linkify=True):
705
    """Format an item link in Markdown."""
706
    if linkify and is_item(item):
707
        if item.header:
708
            return "[{u} {h}]({p}.html#{u})".format(
709
                u=item.uid, h=item.header, p=item.document.prefix
710
            )
711
        return "[{u}]({p}.html#{u})".format(u=item.uid, p=item.document.prefix)
712
    else:
713
        return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
714
715
716
def _format_html_item_link(item, linkify=True):
717
    """Format an item link in HTML."""
718
    if linkify and is_item(item):
719
        if item.header:
720
            link = '<a href="{p}.html#{u}">{u} {h}</a>'.format(
721
                u=item.uid, h=item.header, p=item.document.prefix
722
            )
723
        else:
724
            link = '<a href="{p}.html#{u}">{u}</a>'.format(
725
                u=item.uid, p=item.document.prefix
726
            )
727
        return link
728
    else:
729
        return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
730
731
732
def _format_md_label_links(label, links, linkify):
733
    """Join a string of label and links with formatting."""
734
    if linkify:
735
        return "*{lb}* {ls}".format(lb=label, ls=links)
736
    else:
737
        return "*{lb} {ls}*".format(lb=label, ls=links)
738
739
740
def _table_of_contents_md(obj, linkify=None):
741
    toc = "### Table of Contents\n\n"
742
743
    for item in iter_items(obj):
744
        if item.depth == 1:
745
            prefix = " * "
746
        else:
747
            prefix = "    " * (item.depth - 1)
748
            prefix += "* "
749
750
        if item.heading:
751
            lines = item.text.splitlines()
752
            heading = lines[0] if lines else ""
753
        elif item.header:
754
            heading = "{h}".format(h=item.header)
755
        else:
756
            heading = item.uid
757
758
        if settings.PUBLISH_HEADING_LEVELS:
759
            level = _format_level(item.level)
760
            lbl = "{lev} {h}".format(lev=level, h=heading)
761
        else:
762
            lbl = heading
763
764
        if linkify:
765
            line = "{p}[{lbl}](#{uid})\n".format(p=prefix, lbl=lbl, uid=item.uid)
766
        else:
767
            line = "{p}{lbl}\n".format(p=prefix, lbl=lbl)
768
        toc += line
769
    return toc
770
771
772
def _lines_html(
773
    obj, linkify=False, extensions=EXTENSIONS, template=HTMLTEMPLATE, toc=True
774
):
775
    """Yield lines for an HTML report.
776
777
    :param obj: Item, list of Items, or Document to publish
778
    :param linkify: turn links into hyperlinks
779
780
    :return: iterator of lines of text
781
782
    """
783
    # Determine if a full HTML document should be generated
784
    try:
785
        iter(obj)
786
    except TypeError:
787
        document = False
788
    else:
789
        document = True
790
    # Generate HTML
791
792
    text = "\n".join(_lines_markdown(obj, linkify=linkify))
793
    body = markdown.markdown(text, extensions=extensions)
794
795
    if toc:
796
        toc_md = _table_of_contents_md(obj, True)
797
        toc_html = markdown.markdown(toc_md, extensions=extensions)
798
    else:
799
        toc_html = ""
800
801
    if document:
802
        try:
803
            bottle.TEMPLATE_PATH.insert(
804
                0, os.path.join(os.path.dirname(__file__), "..", "views")
805
            )
806
            if "baseurl" not in bottle.SimpleTemplate.defaults:
807
                bottle.SimpleTemplate.defaults["baseurl"] = ""
808
            html = bottle_template(
809
                template, body=body, toc=toc_html, parent=obj.parent, document=obj
810
            )
811
        except Exception:
812
            log.error("Problem parsing the template %s", template)
813
            raise
814
        yield "\n".join(html.split(os.linesep))
815
    else:
816
        yield body
817
818
819
# Mapping from file extension to lines generator
820
FORMAT_LINES = {
821
    ".txt": _lines_text,
822
    ".md": _lines_markdown,
823
    ".html": _lines_html,
824
    ".tex": _lines_latex,
825
}
826
827
828
def check(ext):
829
    """Confirm an extension is supported for publish.
830
831
    :raises: :class:`doorstop.common.DoorstopError` for unknown formats
832
833
    :return: lines generator if available
834
835
    """
836
    exts = ", ".join(ext for ext in FORMAT_LINES)
837
    msg = "unknown publish format: {} (options: {})".format(ext or None, exts)
838
    exc = DoorstopError(msg)
839
840
    try:
841
        gen = FORMAT_LINES[ext]
842
    except KeyError:
843
        raise exc from None
844
    else:
845
        log.debug("found lines generator for: {}".format(ext))
846
        return gen
847