Passed
Push — develop ( e91dc8...2ae66f )
by Jace
04:37 queued 14s
created

HtmlPublisher.lines()   D

Complexity

Conditions 12

Size

Total Lines 77
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 48
dl 0
loc 77
rs 4.8
c 0
b 0
f 0
cc 12
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like doorstop.core.publishers.html.HtmlPublisher.lines() 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 documents and items."""
4
5
import os
6
import re
7
import tempfile
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.common import DoorstopError
16
from doorstop.core.publishers.base import extract_prefix, extract_uid
17
from doorstop.core.publishers.markdown import MarkdownPublisher
18
from doorstop.core.template import CSS, HTMLTEMPLATE, INDEX, MATRIX
19
from doorstop.core.types import is_item
20
21
log = common.logger(__name__)
22
23
24
class HtmlPublisher(MarkdownPublisher):
25
    """HTML publisher."""
26
27
    def __init__(self, obj, ext):
28
        super().__init__(obj, ext)
29
        # Define lists.
30
        self.list["start"] = {"itemize": "<ul>", "enumerate": "<ol>"}
31
        self.list["end"] = {"itemize": "</ul>", "enumerate": "</ol>"}
32
        self.list["start_item"] = {"itemize": "<li>", "enumerate": "<li>"}
33
        self.list["end_item"] = {"itemize": "</li>", "enumerate": "</li>"}
34
35
    EXTENSIONS = (
36
        "markdown.extensions.extra",
37
        "markdown.extensions.sane_lists",
38
        PlantUMLMarkdownExtension(
39
            server="http://www.plantuml.com/plantuml",
40
            cachedir=tempfile.gettempdir(),
41
            format="svg",
42
            classes="class1,class2",
43
            title="UML",
44
            alt="UML Diagram",
45
        ),
46
    )
47
48
    def create_index(self, directory, index=INDEX, extensions=(".html",), tree=None):
49
        """Create an HTML index of all files in a directory.
50
51
        :param directory: directory for index
52
        :param index: filename for index
53
        :param extensions: file extensions to include
54
        :param tree: optional tree to determine index structure
55
56
        """
57
        # Get paths for the index index
58
        filenames = []
59
        for filename in os.listdir(directory):
60
            if filename.endswith(extensions) and filename != INDEX:
61
                filenames.append(os.path.join(filename))
62
63
        # Create the index
64
        if filenames:
65
            path = os.path.join(directory, index)
66
            log.info("creating an {}...".format(index))
67
            lines = self._lines_index(sorted(filenames), tree=tree)
68
            common.write_lines(lines, path, end=settings.WRITE_LINESEPERATOR)
69
        else:
70
            log.warning("no files for {}".format(index))
71
72
    def _lines_index(self, filenames, charset="UTF-8", tree=None):
73
        """Yield lines of HTML for index.html.
74
75
        :param filesnames: list of filenames to add to the index
76
        :param charset: character encoding for output
77
        :param tree: optional tree to determine index structure
78
79
        """
80
        yield "<!DOCTYPE html>"
81
        yield "<head>"
82
        yield (
83
            '<meta http-equiv="content-type" content="text/html; '
84
            'charset={charset}">'.format(charset=charset)
85
        )
86
        yield '<style type="text/css">'
87
        yield from _lines_css()
88
        yield "</style>"
89
        yield "</head>"
90
        yield "<body>"
91
        # Tree structure
92
        text = tree.draw() if tree else None
93
        if text:
94
            yield ""
95
            yield "<h3>Tree Structure:</h3>"
96
            yield "<pre><code>" + text + "</pre></code>"
97
            yield ""
98
            yield "<hr>"
99
        # Additional files
100
        yield ""
101
        yield "<h3>Published Documents:</h3>"
102
        yield "<p>"
103
        yield "<ul>"
104
        for filename in filenames:
105
            name = os.path.splitext(filename)[0]
106
            yield '<li> <a href="{f}">{n}</a> </li>'.format(f=filename, n=name)
107
        yield "</ul>"
108
        yield "</p>"
109
        # Traceability table
110
        documents = tree.documents if tree else None
111
        if documents:
112
            yield ""
113
            yield "<hr>"
114
            yield ""
115
            # table
116
            yield "<h3>Item Traceability:</h3>"
117
            yield "<p>"
118
            yield "<table>"
119
            # header
120
            for document in documents:  # pylint: disable=not-an-iterable
121
                yield '<col width="100">'
122
            yield "<tr>"
123
            for document in documents:  # pylint: disable=not-an-iterable
124
                link = '<a href="{p}.html">{p}</a>'.format(p=document.prefix)
125
                yield (
126
                    '  <th height="25" align="center"> {link} </th>'.format(link=link)
127
                )
128
            yield "</tr>"
129
            # data
130
            for index, row in enumerate(tree.get_traceability()):
131
                if index % 2:
132
                    yield '<tr class="alt">'
133
                else:
134
                    yield "<tr>"
135
                for item in row:
136
                    if item is None:
137
                        link = ""
138
                    else:
139
                        link = self.format_item_link(item)
140
                    yield '  <td height="25" align="center"> {} </td>'.format(link)
141
                yield "</tr>"
142
            yield "</table>"
143
            yield "</p>"
144
        yield ""
145
        yield "</body>"
146
        yield "</html>"
147
148
    def create_matrix(self, directory):
149
        """Create a traceability matrix for all the items.
150
151
        :param directory: directory for matrix
152
153
        """
154
        # Get path and format extension
155
        filename = MATRIX
156
        path = os.path.join(directory, filename)
157
        # ext = self.ext or os.path.splitext(path)[-1] or ".csv"
158
159
        # Create the matrix
160
        log.info("creating an {}...".format(filename))
161
        content = self._matrix_content()
162
        common.write_csv(content, path)
163
164
    def _matrix_content(self):
165
        """Yield rows of content for the traceability matrix."""
166
        yield tuple(map(extract_prefix, self.object.documents))
167
        for row in self.object.get_traceability():
168
            yield tuple(map(extract_uid, row))
169
170 View Code Duplication
    def format_item_link(self, item, linkify=True):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
171
        """Format an item link in HTML."""
172
        if linkify and is_item(item):
173
            if item.header:
174
                link = '<a href="{p}.html#{u}">{u} {h}</a>'.format(
175
                    u=item.uid, h=item.header, p=item.document.prefix
176
                )
177
            else:
178
                link = '<a href="{p}.html#{u}">{u}</a>'.format(
179
                    u=item.uid, p=item.document.prefix
180
                )
181
            return link
182
        else:
183
            return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
184
185
    def lines(self, obj, **kwargs):
186
        """Yield lines for an HTML report.
187
188
        :param obj: Item, list of Items, or Document to publish
189
        :param linkify: turn links into hyperlinks
190
191
        :return: iterator of lines of text
192
193
        """
194
        linkify = kwargs.get("linkify", False)
195
        toc = kwargs.get("toc", False)
196
197
        # Determine if a full HTML document should be generated
198
        try:
199
            iter(obj)
200
        except TypeError:
201
            document = False
202
        else:
203
            document = True
204
205
        # Generate HTML
206
        text = "\n".join(self._lines_markdown(obj, linkify=linkify, to_html=True))
207
        # We need to handle escaped back-ticks before we pass the text to markdown.
208
        text = text.replace("\\`", "##!!TEMPINLINE!!##")
209
        body_to_check = markdown.markdown(text, extensions=self.EXTENSIONS).splitlines()
210
        block = []
211
        # Check for nested lists since they are not supported by the markdown_sane_lists plugin.
212
        for i, line in enumerate(body_to_check):
213
            # Replace the temporary inline code blocks with the escaped back-ticks. If there are
214
            # multiple back-ticks in a row, we need group them in a single <code> block.
215
            line = re.sub(
216
                r"(##!!TEMPINLINE!!##)+",
217
                lambda m: "<code>" + "&#96;" * int(len(m.group()) / 18) + "</code>",
218
                line,
219
            )
220
221
            # line = line.replace("##!!TEMPINLINE!!##", "<code>&#96;</code>")
222
            # Check if we are at the end of the body.
223
            if i == len(body_to_check) - 1:
224
                next_line = ""
225
            else:
226
                next_line = body_to_check[i + 1]
227
            (_, processed_block, processed_line) = self.process_lists(line, next_line)
228
            if processed_block != "":
229
                block.append(processed_block)
230
            block.append(processed_line)
231
        body = "\n".join(block)
232
233
        if toc:
234
            toc_md = self.table_of_contents(True, obj)
235
            toc_html = markdown.markdown(toc_md, extensions=self.EXTENSIONS)
236
        else:
237
            toc_html = ""
238
239
        if document:
240
            if self.template == "":
241
                self.template = HTMLTEMPLATE
242
            try:
243
                bottle.TEMPLATE_PATH.insert(
244
                    0, os.path.join(os.path.dirname(__file__), "..", "..", "views")
245
                )
246
                if "baseurl" not in bottle.SimpleTemplate.defaults:
247
                    bottle.SimpleTemplate.defaults["baseurl"] = ""
248
                html = bottle_template(
249
                    self.template,
250
                    body=body,
251
                    toc=toc_html,
252
                    parent=obj.parent,
253
                    document=obj,
254
                )
255
            except Exception:
256
                raise DoorstopError(
257
                    "Problem parsing the template {}".format(self.template)
258
                )
259
            yield "\n".join(html.split(os.linesep))
260
        else:
261
            yield body
262
263
264
def _lines_css():
265
    """Yield lines of CSS to embedded in HTML."""
266
    yield ""
267
    for line in common.read_lines(CSS):
268
        yield line.rstrip()
269
    yield ""
270