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

doorstop.core.publisher._table_of_contents_md()   C

Complexity

Conditions 9

Size

Total Lines 33
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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