Passed
Push — develop ( 73032f...a17c0d )
by Jace
02:20 queued 47s
created

doorstop.core.publishers.markdown   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 336
Duplicated Lines 13.1 %

Importance

Changes 0
Metric Value
wmc 65
eloc 199
dl 44
loc 336
rs 3.2
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A MarkdownPublisher.create_matrix() 0 2 1
C MarkdownPublisher._generate_heading_from_item() 0 43 10
A MarkdownPublisher.format_label_links() 0 6 2
F MarkdownPublisher._lines_markdown() 10 68 14
A MarkdownPublisher.lines_index() 0 16 4
A MarkdownPublisher.create_index() 0 24 5
A MarkdownPublisher._index_tree() 0 18 2
A MarkdownPublisher.format_links() 0 7 2
A MarkdownPublisher.lines() 0 15 2
A MarkdownPublisher.format_item_link() 0 13 4
A MarkdownPublisher.format_references() 23 23 5
B MarkdownPublisher.table_of_contents() 0 35 8
A MarkdownPublisher.format_ref() 11 11 3
A MarkdownPublisher.format_attr_list() 0 3 2

1 Function

Rating   Name   Duplication   Size   Complexity  
A clean_link() 0 13 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like doorstop.core.publishers.markdown 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
from re import sub
7
8
from doorstop import common, settings
9
from doorstop.core.publishers.base import (
10
    BasePublisher,
11
    extract_prefix,
12
    format_level,
13
    get_document_attributes,
14
)
15
from doorstop.core.types import is_item, iter_items
16
17
log = common.logger(__name__)
18
INDEX = "index.md"
19
20
21
class MarkdownPublisher(BasePublisher):
22
    """Markdown publisher."""
23
24
    def create_index(self, directory, index=INDEX, extensions=(".md",), tree=None):
25
        """Create an markdown index of all files in a directory.
26
27
        :param directory: directory for index
28
        :param index: filename for index
29
        :param extensions: file extensions to include
30
        :param tree: optional tree to determine index structure
31
32
        """
33
        # Get paths for the index index
34
        filenames = []
35
        for filename in os.listdir(directory):
36
            if filename.endswith(extensions) and filename != INDEX:
37
                filenames.append(os.path.join(filename))
38
39
        # Create the index
40
        if filenames:
41
            path = os.path.join(directory, index)
42
            log.info("creating an {}...".format(index))
43
            lines = self.lines_index(sorted(filenames), tree=tree)
44
            common.write_text(" # Requirements index", path)
45
            common.write_text("\n".join(lines), path)
46
        else:
47
            log.warning("no files for {}".format(index))
48
49
    def _index_tree(self, tree, depth):
50
        """Recursively generate markdown index.
51
52
        :param tree: optional tree to determine index structure
53
        :param depth: depth recursed into tree
54
        """
55
56
        depth = depth + 1
57
58
        title = get_document_attributes(tree.document)["title"]
59
        prefix = extract_prefix(tree.document)
60
        filename = f"{prefix}.md"
61
62
        # Tree structure
63
        yield " " * (depth * 2 - 1) + f"* [{prefix}]({filename}) - {title}"
64
        # yield self.table_of_contents(linkify=True, obj=tree.document, depth=depth, heading=False)
65
        for child in tree.children:
66
            yield from self._index_tree(tree=child, depth=depth)
67
68
    def lines_index(self, filenames, tree=None):
69
        """Yield lines of Markdown for index.md.
70
71
        :param filenames: list of filenames to add to the index
72
        :param tree: optional tree to determine index structure
73
        """
74
        if tree:
75
            yield from self._index_tree(tree, depth=0)
76
77
        # Additional files
78
        if filenames:
79
            yield ""
80
            yield "### Published Documents:"
81
            for filename in filenames:
82
                name = os.path.splitext(filename)[0]
83
                yield " * [{n}]({f})".format(f=filename, n=name)
84
85
    def create_matrix(self, directory):
86
        """No traceability matrix for Markdown."""
87
88
    def format_attr_list(self, item, linkify):
89
        """Create a Markdown attribute list for a heading."""
90
        return " {{#{u}}}".format(u=item.uid) if linkify else ""
91
92 View Code Duplication
    def format_ref(self, item):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
93
        """Format an external reference in Markdown."""
94
        if settings.CHECK_REF:
95
            path, line = item.find_ref()
96
            path = path.replace("\\", "/")  # always use unix-style paths
97
            if line:
98
                return "> `{p}` (line {line})".format(p=path, line=line)
99
            else:
100
                return "> `{p}`".format(p=path)
101
        else:
102
            return "> '{r}'".format(r=item.ref)
103
104 View Code Duplication
    def format_references(self, item):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
105
        """Format an external reference in Markdown."""
106
        if settings.CHECK_REF:
107
            references = item.find_references()
108
            text_refs = []
109
            for ref_item in references:
110
                path, line = ref_item
111
                path = path.replace("\\", "/")  # always use unix-style paths
112
113
                if line:
114
                    text_refs.append("> `{p}` (line {line})".format(p=path, line=line))
115
                else:
116
                    text_refs.append("> `{p}`".format(p=path))
117
118
            return "\n".join(ref for ref in text_refs)
119
        else:
120
            references = item.references
121
            text_refs = []
122
            for ref_item in references:
123
                path = ref_item["path"]
124
                path = path.replace("\\", "/")  # always use unix-style paths
125
                text_refs.append("> '{r}'".format(r=path))
126
            return "\n".join(ref for ref in text_refs)
127
128
    def format_links(self, items, linkify):
129
        """Format a list of linked items in Markdown."""
130
        links = []
131
        for item in items:
132
            link = self.format_item_link(item, linkify=linkify)
133
            links.append(link)
134
        return ", ".join(links)
135
136
    def format_item_link(self, item, linkify=True):
137
        """Format an item link in Markdown."""
138
        if linkify and is_item(item):
139
            link = clean_link("{u}".format(u=self._generate_heading_from_item(item)))
140
            if item.header:
141
                return "[{u} {h}]({p}.md#{l})".format(
142
                    u=item.uid, l=link, h=item.header, p=item.document.prefix
143
                )
144
            return "[{u}]({p}.md#{l})".format(
145
                u=item.uid, l=link, p=item.document.prefix
146
            )
147
        else:
148
            return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
149
150
    def format_label_links(self, label, links, linkify):
151
        """Join a string of label and links with formatting."""
152
        if linkify:
153
            return "*{lb}* {ls}".format(lb=label, ls=links)
154
        else:
155
            return "*{lb} {ls}*".format(lb=label, ls=links)
156
157
    def table_of_contents(self, linkify=None, obj=None):
158
        """Generate a table of contents for a Markdown document."""
159
160
        toc = "### Table of Contents\n\n"
161
        toc_doc = obj
162
163
        for item in iter_items(toc_doc):
164
            if item.depth == 1:
165
                prefix = " * "
166
            else:
167
                prefix = "    " * (item.depth - 1)
168
                prefix += "* "
169
170
            # Check if item has the attribute heading.
171
            if item.heading:
172
                lines = item.text.splitlines()
173
                heading = lines[0] if lines else ""
174
            elif item.header:
175
                heading = "{h}".format(h=item.header)
176
            else:
177
                heading = item.uid
178
179
            if settings.PUBLISH_HEADING_LEVELS:
180
                level = format_level(item.level)
181
                lbl = "{lev} {h}".format(lev=level, h=heading)
182
            else:
183
                lbl = heading
184
185
            if linkify:
186
                link = clean_link(self._generate_heading_from_item(item))
187
                line = "{p}[{lbl}](#{l})\n".format(p=prefix, lbl=lbl, l=link)
188
            else:
189
                line = "{p}{lbl}\n".format(p=prefix, lbl=lbl)
190
            toc += line
191
        return toc
192
193
    def lines(self, obj, **kwargs):
194
        """Yield lines for a Markdown report.
195
196
        :param obj: Item, list of Items, or Document to publish
197
        :param linkify: turn links into hyperlinks
198
199
        :return: iterator of lines of text
200
201
        """
202
        linkify = kwargs.get("linkify", False)
203
        toc = kwargs.get("toc", False)
204
        if toc:
205
            yield self.table_of_contents(linkify=linkify, obj=obj)
206
207
        yield from self._lines_markdown(obj, **kwargs)
208
209
    def _generate_heading_from_item(self, item, to_html=False):
210
        """Generate a heading from an item in a consistent way for Markdown.
211
212
        This ensures that references between documents are consistent.
213
        """
214
        result = ""
215
        heading = "#" * item.depth
216
        level = format_level(item.level)
217
        if item.heading:
218
            text_lines = item.text.splitlines()
219
            if item.header:
220
                text_lines.insert(0, item.header)
221
            # Level and Text
222
            if settings.PUBLISH_HEADING_LEVELS:
223
                standard = "{h} {lev} {t}".format(
224
                    h=heading, lev=level, t=text_lines[0] if text_lines else ""
225
                )
226
            else:
227
                standard = "{h} {t}".format(
228
                    h=heading, t=text_lines[0] if text_lines else ""
229
                )
230
            attr_list = self.format_attr_list(item, True)
231
            result = standard + attr_list
232
        else:
233
            uid = item.uid
234
            if settings.ENABLE_HEADERS:
235
                if item.header:
236
                    if to_html:
237
                        uid = "{h} <small>{u}</small>".format(h=item.header, u=item.uid)
238
                    else:
239
                        uid = "{h} _{u}_".format(h=item.header, u=item.uid)
240
                else:
241
                    uid = "{u}".format(u=item.uid)
242
243
            # Level and UID
244
            if settings.PUBLISH_BODY_LEVELS:
245
                standard = "{h} {lev} {u}".format(h=heading, lev=level, u=uid)
246
            else:
247
                standard = "{h} {u}".format(h=heading, u=uid)
248
249
            attr_list = self.format_attr_list(item, True)
250
            result = standard + attr_list
251
        return result
252
253
    def _lines_markdown(self, obj, **kwargs):
254
        """Yield lines for a Markdown report.
255
256
        :param obj: Item, list of Items, or Document to publish
257
        :param linkify: turn links into hyperlinks
258
259
        :return: iterator of lines of text
260
261
        """
262
        linkify = kwargs.get("linkify", False)
263
        to_html = kwargs.get("to_html", False)
264
        for item in iter_items(obj):
265
            # Create iten heading.
266
            complete_heading = self._generate_heading_from_item(item, to_html=to_html)
267
            yield complete_heading
268
269
            # Text
270
            if item.text:
271
                yield ""  # break before text
272
                yield from item.text.splitlines()
273
274
            # Reference
275
            if item.ref:
276
                yield ""  # break before reference
277
                yield self.format_ref(item)
278
279
            # Reference
280
            if item.references:
281
                yield ""  # break before reference
282
                yield self.format_references(item)
283
284
            # Parent links
285 View Code Duplication
            if item.links:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
286
                yield ""  # break before links
287
                items2 = item.parent_items
288
                if settings.PUBLISH_CHILD_LINKS:
289
                    label = "Parent links:"
290
                else:
291
                    label = "Links:"
292
                links = self.format_links(items2, linkify)
293
                label_links = self.format_label_links(label, links, linkify)
294
                yield label_links
295
296
            # Child links
297
            if settings.PUBLISH_CHILD_LINKS:
298
                items2 = item.find_child_items()
299
                if items2:
300
                    yield ""  # break before links
301
                    label = "Child links:"
302
                    links = self.format_links(items2, linkify)
303
                    label_links = self.format_label_links(label, links, linkify)
304
                    yield label_links
305
306
            # Add custom publish attributes
307
            if item.document and item.document.publish:
308
                header_printed = False
309
                for attr in item.document.publish:
310
                    if not item.attribute(attr):
311
                        continue
312
                    if not header_printed:
313
                        header_printed = True
314
                        yield ""
315
                        yield "| Attribute | Value |"
316
                        yield "| --------- | ----- |"
317
                    yield "| {} | {} |".format(attr, item.attribute(attr))
318
                yield ""
319
320
            yield ""  # break between items
321
322
323
def clean_link(uid):
324
    """Clean a UID for use in a link.
325
326
    1. Strip leading # and spaces.
327
    2. Only smallcaps are allowed.
328
    3. Spaces are replaced with hyphens.
329
    5. All other special characters are removed.
330
    """
331
    uid = sub(r"^#*\s*", "", uid)
332
    uid = uid.lower()
333
    uid = uid.replace(" ", "-")
334
    uid = sub("[^a-z0-9-]", "", uid)
335
    return uid
336