doorstop.core.publishers.markdown   C
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 271
Duplicated Lines 16.24 %

Importance

Changes 0
Metric Value
wmc 55
eloc 165
dl 44
loc 271
rs 6
c 0
b 0
f 0

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