jrnl.Entry.Entry.__init__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nop 5
dl 0
loc 9
rs 9.95
c 0
b 0
f 0
1
#!/usr/bin/env python
2
3
from datetime import datetime
4
import re
5
6
import ansiwrap
7
8
from .color import colorize
9
from .color import highlight_tags_with_background_color
10
11
12
class Entry:
13
    def __init__(self, journal, date=None, text="", starred=False):
14
        self.journal = journal  # Reference to journal mainly to access its config
15
        self.date = date or datetime.now()
16
        self.text = text
17
        self._title = None
18
        self._body = None
19
        self._tags = None
20
        self.starred = starred
21
        self.modified = False
22
23
    @property
24
    def fulltext(self):
25
        return self.title + " " + self.body
26
27
    def _parse_text(self):
28
        raw_text = self.text
29
        lines = raw_text.splitlines()
30
        if lines and lines[0].strip().endswith("*"):
31
            self.starred = True
32
            raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:])
33
        self._title, self._body = split_title(raw_text)
34
        if self._tags is None:
35
            self._tags = list(self._parse_tags())
36
37
    @property
38
    def title(self):
39
        if self._title is None:
40
            self._parse_text()
41
        return self._title
42
43
    @title.setter
44
    def title(self, x):
45
        self._title = x
46
47
    @property
48
    def body(self):
49
        if self._body is None:
50
            self._parse_text()
51
        return self._body
52
53
    @body.setter
54
    def body(self, x):
55
        self._body = x
56
57
    @property
58
    def tags(self):
59
        if self._tags is None:
60
            self._parse_text()
61
        return self._tags
62
63
    @tags.setter
64
    def tags(self, x):
65
        self._tags = x
66
67
    @staticmethod
68
    def tag_regex(tagsymbols):
69
        pattern = fr"(?<!\S)([{tagsymbols}][-+*#/\w]+)"
70
        return re.compile(pattern)
71
72
    def _parse_tags(self):
73
        tagsymbols = self.journal.config["tagsymbols"]
74
        return {
75
            tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)
76
        }
77
78
    def __str__(self):
79
        """Returns a string representation of the entry to be written into a journal file."""
80
        date_str = self.date.strftime(self.journal.config["timeformat"])
81
        title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
82
        if self.starred:
83
            title += " *"
84
        return "{title}{sep}{body}\n".format(
85
            title=title,
86
            sep="\n" if self.body.rstrip("\n ") else "",
87
            body=self.body.rstrip("\n "),
88
        )
89
90
    def pprint(self, short=False):
91
        """Returns a pretty-printed version of the entry.
92
        If short is true, only print the title."""
93
        # Handle indentation
94
        if self.journal.config["indent_character"]:
95
            indent = self.journal.config["indent_character"].rstrip() + " "
96
        else:
97
            indent = ""
98
99
        date_str = colorize(
100
            self.date.strftime(self.journal.config["timeformat"]),
101
            self.journal.config["colors"]["date"],
102
            bold=True,
103
        )
104
105
        if not short and self.journal.config["linewrap"]:
106
            # Color date / title and bold title
107
            title = ansiwrap.fill(
108
                date_str
109
                + " "
110
                + highlight_tags_with_background_color(
111
                    self,
112
                    self.title,
113
                    self.journal.config["colors"]["title"],
114
                    is_title=True,
115
                ),
116
                self.journal.config["linewrap"],
117
            )
118
            body = highlight_tags_with_background_color(
119
                self, self.body.rstrip(" \n"), self.journal.config["colors"]["body"]
120
            )
121
            body_text = [
122
                colorize(
123
                    ansiwrap.fill(
124
                        line,
125
                        self.journal.config["linewrap"],
126
                        initial_indent=indent,
127
                        subsequent_indent=indent,
128
                        drop_whitespace=True,
129
                    ),
130
                    self.journal.config["colors"]["body"],
131
                )
132
                or indent
133
                for line in body.rstrip(" \n").splitlines()
134
            ]
135
136
            # ansiwrap doesn't handle lines with only the "\n" character and some
137
            # ANSI escapes properly, so we have this hack here to make sure the
138
            # beginning of each line has the indent character and it's colored
139
            # properly. textwrap doesn't have this issue, however, it doesn't wrap
140
            # the strings properly as it counts ANSI escapes as literal characters.
141
            # TL;DR: I'm sorry.
142
            body = "\n".join(
143
                [
144
                    colorize(indent, self.journal.config["colors"]["body"]) + line
145
                    if not ansiwrap.strip_color(line).startswith(indent)
146
                    else line
147
                    for line in body_text
148
                ]
149
            )
150
        else:
151
            title = (
152
                date_str
153
                + " "
154
                + highlight_tags_with_background_color(
155
                    self,
156
                    self.title.rstrip("\n"),
157
                    self.journal.config["colors"]["title"],
158
                    is_title=True,
159
                )
160
            )
161
            body = highlight_tags_with_background_color(
162
                self, self.body.rstrip("\n "), self.journal.config["colors"]["body"]
163
            )
164
165
        # Suppress bodies that are just blanks and new lines.
166
        has_body = len(self.body) > 20 or not all(
167
            char in (" ", "\n") for char in self.body
168
        )
169
170
        if short:
171
            return title
172
        else:
173
            return "{title}{sep}{body}\n".format(
174
                title=title, sep="\n" if has_body else "", body=body if has_body else ""
175
            )
176
177
    def __repr__(self):
178
        return "<Entry '{}' on {}>".format(
179
            self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")
180
        )
181
182
    def __hash__(self):
183
        return hash(self.__repr__())
184
185
    def __eq__(self, other):
186
        if (
187
            not isinstance(other, Entry)
188
            or self.title.strip() != other.title.strip()
189
            or self.body.rstrip() != other.body.rstrip()
190
            or self.date != other.date
191
            or self.starred != other.starred
192
        ):
193
            return False
194
        return True
195
196
    def __ne__(self, other):
197
        return not self.__eq__(other)
198
199
200
# Based on Segtok by Florian Leitner
201
# https://github.com/fnl/segtok
202
SENTENCE_SPLITTER = re.compile(
203
    r"""
204
(                       # A sentence ends at one of two sequences:
205
    [.!?\u2026\u203C\u203D\u2047\u2048\u2049\u22EF\u3002\uFE52\uFE57\uFF01\uFF0E\uFF1F\uFF61]                # Either, a sequence starting with a sentence terminal,
206
    [\'\u2019\"\u201D]? # an optional right quote,
207
    [\]\)]*             # optional closing brackets and
208
    \s+                 # a sequence of required spaces.
209
)""",
210
    re.VERBOSE,
211
)
212
SENTENCE_SPLITTER_ONLY_NEWLINE = re.compile("\n")
213
214
215
def split_title(text):
216
    """Splits the first sentence off from a text."""
217
    sep = SENTENCE_SPLITTER_ONLY_NEWLINE.search(text.lstrip())
218
    if not sep:
219
        sep = SENTENCE_SPLITTER.search(text)
220
        if not sep:
221
            return text, ""
222
    return text[: sep.end()].strip(), text[sep.end() :].strip()
223