Passed
Push — develop ( f34c45...4326c3 )
by
unknown
05:23 queued 15s
created

HtmlPublisher.publishAction()   A

Complexity

Conditions 4

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 22
rs 9.9
c 0
b 0
f 0
cc 4
nop 3
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.core.publishers.base import (
16
    extract_prefix,
17
    extract_uid,
18
    format_level,
19
    get_document_attributes,
20
)
21
from doorstop.core.publishers.markdown import MarkdownPublisher
22
from doorstop.core.template import HTMLTEMPLATE, INDEX, MATRIX
23
from doorstop.core.types import is_item, iter_items
24
25
log = common.logger(__name__)
26
27
28
class HtmlPublisher(MarkdownPublisher):
29
    """HTML publisher."""
30
31
    def __init__(self, obj, ext):
32
        super().__init__(obj, ext)
33
        # Define lists.
34
        self.list["start"] = {"itemize": "<ul>", "enumerate": "<ol>"}
35
        self.list["end"] = {"itemize": "</ul>", "enumerate": "</ol>"}
36
        self.list["start_item"] = {"itemize": "<li>", "enumerate": "<li>"}
37
        self.list["end_item"] = {"itemize": "</li>", "enumerate": "</li>"}
38
39
    EXTENSIONS = (
40
        "markdown.extensions.extra",
41
        "markdown.extensions.sane_lists",
42
        PlantUMLMarkdownExtension(
43
            server="http://www.plantuml.com/plantuml",
44
            cachedir=tempfile.gettempdir(),
45
            format="svg",
46
            classes="class1,class2",
47
            title="UML",
48
            alt="UML Diagram",
49
        ),
50
    )
51
52
    def publishAction(self, document, path):
53
        """Publish action.
54
55
        Replace this with code that should be run _for each_ document
56
        _during_ publishing.
57
        """
58
        self.document = document
59
        # Check if path ends with .html
60
        if path.endswith(".html"):
61
            # Split of the filename and add 'documents/' to the path.
62
            documentPath = os.path.join(os.path.dirname(path), "documents")
63
        else:
64
            # Add 'documents/' to the path.
65
            documentPath = os.path.join(path, "documents")
66
        # Create the document directory if it does not exist.
67
        if not os.path.exists(documentPath):
68
            os.makedirs(documentPath)
69
        # Check if path ends with .html
70
        if path.endswith(".html"):
71
            self.documentPath = os.path.join(documentPath, os.path.basename(path))
72
        else:
73
            self.documentPath = os.path.join(documentPath, document.prefix + ".html")
74
75
    def create_index(self, directory, index=INDEX, extensions=(".html",), tree=None):
76
        """Create an HTML index of all files in a directory.
77
78
        :param directory: directory for index
79
        :param index: filename for index
80
        :param extensions: file extensions to include
81
        :param tree: optional tree to determine index structure
82
83
        """
84
        # Get paths for the index index
85
        filenames = []
86
        tmpPath = os.path.join(directory, "documents")
87
        if os.path.exists(tmpPath):
88
            for filename in os.listdir(tmpPath):
89
                if filename.endswith(extensions) and filename != INDEX:
90
                    filenames.append(os.path.join(filename))
91
92
        # Create the index
93
        if filenames:
94
            path = os.path.join(directory, index)
95
            log.info("creating an {}...".format(index))
96
            lines = self.lines_index(sorted(filenames), tree=tree)
97
            # Format according to the template.
98
            templatePath = os.path.abspath(
99
                os.path.join(self.assetsPath, "..", "..", "template", "views")
100
            )
101
            html = self.typesetTemplate(
102
                templatePath,
103
                "\n".join(lines),
104
                doc_attributes={
105
                    "name": "Index",
106
                    "ref": "-",
107
                    "title": "Doorstop index",
108
                    "by": "-",
109
                    "major": "-",
110
                    "minor": "",
111
                },
112
            )
113
            common.write_text(html, path)
114
        else:
115
            log.warning("no files for {}".format(index))
116
117
    def lines_index(self, filenames, tree=None):
118
        """Yield lines of HTML for index.html.
119
120
        :param filesnames: list of filenames to add to the index
121
        :param tree: optional tree to determine index structure
122
123
        """
124
        # Tree structure
125
        text = tree.draw(html_links=True) if tree else None
126
        yield ""
127
        yield "<h3>Tree Structure:</h3>"
128
        yield "<pre><code>" + text + "</pre></code>"
129
        yield ""
130
        yield "<hr>"
131
        # Additional files
132
        yield ""
133
        yield "<h3>Published Documents:</h3>"
134
        yield "<p>"
135
        yield "<ul>"
136
        for filename in filenames:
137
            name = os.path.splitext(filename)[0]
138
            yield '<li> <a href="documents/{f}">{n}</a> </li>'.format(
139
                f=filename, n=name
140
            )
141
        yield "</ul>"
142
        yield "</p>"
143
144
    def create_matrix(self, directory):
145
        """Create a traceability matrix for all the items. This will create a .csv and .html file.
146
147
        :param directory: directory for matrix
148
149
        """
150
        ############################################################
151
        # Create the csv matrix
152
        ############################################################
153
        # Get path and format extension
154
        filename = MATRIX
155
        path = os.path.join(directory, filename)
156
157
        # Create the matrix
158
        log.info("creating an {}...".format(filename))
159
        content = self._matrix_content()
160
        common.write_csv(content, path)
161
162
        ############################################################
163
        # Create the HTML matrix
164
        ############################################################
165
        filename = MATRIX.replace(".csv", ".html")
166
        path = os.path.join(directory, filename)
167
        log.info("creating an {}...".format(filename))
168
        lines = self.lines_matrix()
169
        # Format according to the template.
170
        if self.template == "":
171
            self.template = HTMLTEMPLATE
172
        templatePath = os.path.abspath(
173
            os.path.join(self.assetsPath, "..", "..", "template", "views")
174
        )
175
        html = self.typesetTemplate(
176
            templatePath,
177
            "\n".join(lines),
178
            doc_attributes={
179
                "name": "Traceability",
180
                "ref": "-",
181
                "title": "Doorstop traceability matrix",
182
                "by": "-",
183
                "major": "-",
184
                "minor": "",
185
            },
186
        )
187
        common.write_text(html, path)
188
189
    def typesetTemplate(
190
        self,
191
        templatePath,
192
        body,
193
        doc_attributes,
194
        toc=None,
195
        parent=None,
196
        document=None,
197
        is_doc=False,
198
        has_index=False,
199
        has_matrix=False,
200
    ):
201
        """Typeset the template."""
202
        bottle.TEMPLATE_PATH.insert(0, templatePath)
203
        if "baseurl" not in bottle.SimpleTemplate.defaults:
204
            bottle.SimpleTemplate.defaults["baseurl"] = ""
205
        html = bottle_template(
206
            self.template,
207
            body=body,
208
            toc=toc,
209
            parent=parent,
210
            document=document,
211
            doc_attributes=doc_attributes,
212
            is_doc=is_doc,
213
            has_index=has_index,
214
            has_matrix=has_matrix,
215
        )
216
        return html
217
218
    def _matrix_content(self):
219
        """Yield rows of content for the traceability matrix in csv format."""
220
        yield tuple(map(extract_prefix, self.object.documents))
221
        for row in self.object.get_traceability():
222
            yield tuple(map(extract_uid, row))
223
224
    def lines_matrix(self):
225
        """Traceability table for html output."""
226
        yield '<table class="table">'
227
        # header
228
        yield "<thead>"
229
        yield "<tr>"
230
        for document in self.object:  # pylint: disable=not-an-iterable
231
            link = '<a href="documents/{p}.html">{p}</a>'.format(p=document.prefix)
232
            yield ('  <th scope="col">{link}</th>'.format(link=link))
233
        yield "</tr>"
234
        yield "</thead>"
235
        # data
236
        yield "<tbody>"
237
        for index, row in enumerate(self.object.get_traceability()):
238
            if index % 2:
239
                yield '<tr class="alt">'
240
            else:
241
                yield "<tr>"
242
            for item in row:
243
                if item is None:
244
                    link = ""
245
                else:
246
                    link = self.format_item_link(item, is_doc=False)
247
                yield '  <td scope="row">{}</td>'.format(link)
248
            yield "</tr>"
249
        yield "</tbody>"
250
        yield "</table>"
251
252
    def format_item_link(self, item, linkify=True, is_doc=True):
253
        """Format an item link in HTML."""
254
        if linkify and is_item(item):
255
            if is_doc:
256
                tmpRef = ""
257
            else:
258
                tmpRef = "documents/"
259
            if item.header:
260
                link = '<a href="{r}{p}.html#{u}">{u} {h}</a>'.format(
261
                    u=item.uid, h=item.header, p=item.document.prefix, r=tmpRef
262
                )
263
            else:
264
                link = '<a href="{r}{p}.html#{u}">{u}</a>'.format(
265
                    u=item.uid, p=item.document.prefix, r=tmpRef
266
                )
267
            return link
268
        else:
269
            return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
270
271
    def lines(self, obj, **kwargs):
272
        """Yield lines for an HTML report.
273
274
        :param obj: Item, list of Items, or Document to publish
275
        :param linkify: turn links into hyperlinks
276
277
        :return: iterator of lines of text
278
279
        """
280
        linkify = kwargs.get("linkify", False)
281
        toc = kwargs.get("toc", False)
282
283
        # Determine if a full HTML document should be generated
284
        try:
285
            iter(obj)
286
        except TypeError:
287
            document = False
288
        else:
289
            document = True
290
291
        # Check for defined document attributes.
292
        if document:
293
            doc_attributes = get_document_attributes(
294
                obj, is_html=True, extensions=self.EXTENSIONS
295
            )
296
297
        # Generate HTML
298
        text = "\n".join(self._lines_markdown(obj, linkify=linkify, to_html=True))
299
        # We need to handle escaped back-ticks before we pass the text to markdown.
300
        text = text.replace("\\`", "##!!TEMPINLINE!!##")
301
        body_to_check = markdown.markdown(text, extensions=self.EXTENSIONS).splitlines()
302
        block = []
303
        # Check for nested lists since they are not supported by the markdown_sane_lists plugin.
304
        for i, line in enumerate(body_to_check):
305
            # Replace the temporary inline code blocks with the escaped back-ticks. If there are
306
            # multiple back-ticks in a row, we need group them in a single <code> block.
307
            line = re.sub(
308
                r"(##!!TEMPINLINE!!##)+",
309
                lambda m: "<code>" + "&#96;" * int(len(m.group()) / 18) + "</code>",
310
                line,
311
            )
312
            # Check if we are at the end of the body.
313
            if i == len(body_to_check) - 1:
314
                next_line = ""
315
            else:
316
                next_line = body_to_check[i + 1]
317
            (_, processed_block, processed_line) = self.process_lists(line, next_line)
318
            if processed_block != "":
319
                block.append(processed_block)
320
            block.append(processed_line)
321
        body = "\n".join(block)
322
323
        if toc:
324
            toc_html = self.table_of_contents(True, obj)
325
        else:
326
            toc_html = ""
327
328
        if document:
329
            if self.template == "":
330
                self.template = HTMLTEMPLATE
331
            templatePath = os.path.abspath(
332
                os.path.join(self.assetsPath, "..", "..", "template", "views")
333
            )
334
            html = self.typesetTemplate(
335
                templatePath,
336
                body,
337
                doc_attributes,
0 ignored issues
show
introduced by
The variable doc_attributes does not seem to be defined in case document on line 292 is False. Are you sure this can never be the case?
Loading history...
338
                toc=toc_html,
339
                parent=obj.parent,
340
                document=obj,
341
                is_doc=True,
342
                has_index=self.getIndex(),
343
                has_matrix=self.getMatrix(),
344
            )
345
            yield "\n".join(html.split(os.linesep))
346
        else:
347
            yield body
348
349
    def table_of_contents(self, linkify=None, obj=None):
350
        """Generate a table of contents. Returns a nested list of items to be rendered with the template."""
351
        toc = []
352
        toc.append({"depth": 0, "text": "Table of Contents", "uid": "toc"})
353
        toc_doc = obj
354
355
        for item in iter_items(toc_doc):
356
            # Check if item has the attribute heading.
357
            if item.heading:
358
                lines = item.text.splitlines()
359
                heading = lines[0] if lines else ""
360
            elif item.header:
361
                heading = "{h}".format(h=item.header)
362
            else:
363
                heading = item.uid
364
            if settings.PUBLISH_HEADING_LEVELS:
365
                level = format_level(item.level)
366
                lbl = "{lev} {h}".format(lev=level, h=heading)
367
            else:
368
                lbl = heading
369
            if linkify:
370
                uid = item.uid
371
            else:
372
                uid = ""
373
            toc.append({"depth": item.depth, "text": lbl, "uid": uid})
374
        return toc
375