Passed
Push — main ( 3d1548...b97eaa )
by torrua
01:36
created

DefinitionFormatter.highlight_key()   A

Complexity

Conditions 1

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 10
nop 2
dl 0
loc 18
ccs 10
cts 10
cp 1
crap 1
rs 9.9
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2 1
"""
3
HTML Export extensions of LOD database models
4
"""
5 1
import os
6 1
from itertools import groupby
7 1
from typing import Optional, Union, List
8
9 1
from sqlalchemy import or_
10
11 1
from loglan_db.model_db.base_event import BaseEvent
12 1
from loglan_db.model_export import ExportWord, ExportDefinition
13 1
from loglan_db.model_db.addons.addon_word_getter import AddonWordGetter
14
15 1
DEFAULT_HTML_STYLE = os.getenv("DEFAULT_HTML_STYLE", "ultra")
16
17
18 1
class DefinitionFormatter:
19 1
    @staticmethod
20 1
    def format_body(body: str) -> str:
21
        """
22
        Substitutes tags in the definition's body
23
        Formats punctuation signs
24
        :param body:
25
        :return:
26
        """
27 1
        to_key = '<k>'  # key
28 1
        tc_key = '</k>'
29 1
        to_log = '<l>'  # log
30 1
        tc_log = '</l>'
31
32 1
        return body \
33
            .replace("<", "&lt;").replace(">", "&gt;") \
34
            .replace("«", to_key).replace("»", tc_key) \
35
            .replace("{", to_log).replace("}", tc_log) \
36
            .replace("...", "…").replace("--", "—")
37
38 1
    @staticmethod
39 1
    def highlight_key(def_body, word) -> str:
40
        """
41
        Highlights the current key from the list, deselecting the rest
42
        :param def_body:
43
        :param word:
44
        :return:
45
        """
46
47 1
        to_key = '<k>'  # key
48 1
        tc_key = '</k>'
49
50 1
        word_template_original = f'{to_key}{word}{tc_key}'
51 1
        word_template_temp = f'<do_not_delete>{word}</do_not_delete>'
52 1
        def_body = def_body.replace(word_template_original, word_template_temp)
53 1
        def_body = def_body.replace(to_key, "").replace(tc_key, "")
54 1
        def_body = def_body.replace(word_template_temp, word_template_original)
55 1
        return def_body
56
57 1
    @staticmethod
58 1
    def tagged_word_origin_x(d_source_word, tag):
59 1
        w_origin_x = d_source_word.origin_x \
60
            if d_source_word.origin_x and d_source_word.type.group == "Cpx" else str()
61 1
        return tag % w_origin_x if w_origin_x else str()
62
63 1
    @staticmethod
64 1
    def tagged_word_name(usage, d_source_word, tag):
65 1
        w_name = d_source_word.name if not usage \
66
            else usage.replace("%", d_source_word.name)
67 1
        return tag % w_name
68
69 1
    @staticmethod
70 1
    def tagged_definition_body(body, key_word, tag):
71 1
        definition_body = HTMLExportDefinition.format_body(body)
72 1
        definition_body = HTMLExportDefinition.highlight_key(definition_body, key_word)
73 1
        definition_body = tag % definition_body
74 1
        return definition_body
75
76
77 1
class HTMLExportDefinition(ExportDefinition, DefinitionFormatter):
78
    """
79
    HTMLExportDefinition Class
80
    """
81
82 1
    def export_for_english(self, word: str, style: str = DEFAULT_HTML_STYLE) -> str:
83
        """
84
85
        :param word:
86
        :param style:
87
        :return:
88
        """
89
90
        # de = definition english
91 1
        tags = {
92
            "normal": [
93
                '<span class="dg">(%s)</span>',
94
                '<span class="dt">[%s]</span> ',
95
                ' <span class="db">%s</span>',
96
                f'<span class="definition eng" id={self.id}>%s</span>',
97
                '<div class="d_line">%s</div>',
98
                '<span class="w_name">%s</span>, ',
99
                '<span class="w_origin">&lt;%s&gt;</span> ',
100
            ],
101
            "ultra": [
102
                '(%s)', '[%s] ', ' %s', '<de>%s</de>',
103
                '<ld>%s</ld>', '<wn>%s</wn>, ', '<o>&lt;%s&gt;</o> ', ],
104
        }
105
106 1
        t_d_gram, t_d_tags, t_d_body, t_def, t_def_line, t_word_name, t_word_origin = tags[style]
107
108 1
        gram_form = f'{str(self.slots) if self.slots else ""}' + self.grammar_code
109 1
        def_gram = t_d_gram % gram_form if gram_form else ''
110 1
        def_tags = t_d_tags % self.case_tags.replace("-", "&zwj;-&zwj;") if self.case_tags else ''
111
112 1
        def_body = HTMLExportDefinition.tagged_definition_body(self.body, word, t_d_body)
113 1
        word_name = HTMLExportDefinition.tagged_word_name(self.usage, self.source_word, t_word_name)
114 1
        word_origin_x = HTMLExportDefinition.tagged_word_origin_x(self.source_word, t_word_origin)
115
116 1
        definition = t_def % f'{def_tags}{def_gram}{def_body}'
117 1
        return t_def_line % f'{word_name}{word_origin_x}{definition}'
118
119 1
    def export_for_loglan(self, style: str = DEFAULT_HTML_STYLE) -> str:
120
        """
121
122
        :param style:
123
        :return:
124
        """
125 1
        tags = {
126
            # usage, gram, body, tags, definition
127
            "normal": [
128
                '<span class="du">%s</span> ', '<span class="dg">(%s)</span> ',
129
                '<span class="db">%s</span>', ' <span class="dt">[%s]</span>',
130
                f'<div class="definition log" id={self.id}>%s</div>', ],
131
            "ultra": ['<du>%s</du> ', '(%s) ', '%s', ' [%s]', '<dl>%s</dl>', ],
132
        }
133 1
        t_d_usage, t_d_gram, t_d_body, t_d_tags, t_definition = tags[style]
134
135 1
        def_usage = t_d_usage % self.usage.replace("%", "—") if self.usage else ''
136 1
        gram_form = f"{str(self.slots) if self.slots else ''}" + self.grammar_code
137 1
        def_gram = t_d_gram % gram_form if gram_form else ''
138 1
        def_body = t_d_body % HTMLExportDefinition.format_body(self.body)
139 1
        def_tags = t_d_tags % self.case_tags.replace("-", "&zwj;-&zwj;") if self.case_tags else ''
140 1
        return t_definition % f'{def_usage}{def_gram}{def_body}{def_tags}'
141
142
143 1
class HTMLExportWord(ExportWord, AddonWordGetter):
144
    """
145
    HTMLExportWord Class
146
    """
147
148 1
    @classmethod
149 1
    def html_all_by_name(
150
            cls, name: str, style: str = DEFAULT_HTML_STYLE,
151
            event_id: Union[BaseEvent, int, str] = None,
152
            case_sensitive: bool = False,
153
            partial_results: bool = False) -> Optional[str]:
154
        """
155
        Convert all words found by name into one HTML string
156
        Args:
157
            name: Name of the search word
158
            style: HTML design style
159
            event_id:
160
            case_sensitive:
161
            partial_results:
162
        Returns:
163
164
        """
165
166 1
        words_template = {
167
            "normal": '<div class="words">\n%s\n</div>\n',
168
            "ultra": '<ws>\n%s\n</ws>\n',
169
        }
170
171 1
        if not event_id:
172 1
            event_id = BaseEvent.latest().id
173
174 1
        event_id = int(event_id) if isinstance(event_id, (int, str)) else BaseEvent.id
175
176 1
        words = cls.query.filter(cls.event_start_id <= event_id) \
177
            .filter(or_(cls.event_end_id > event_id, cls.event_end_id.is_(None)))
178
179 1
        if case_sensitive:
180 1
            words = cls.__case_sensitive_words_filter(name, words, partial_results)
181
        else:
182 1
            words = cls.__case_insensitive_words_filter(name, words, partial_results)
183
184 1
        words = words.order_by(cls.name).all()
185
186 1
        if not words:
187 1
            return None
188
189 1
        items = cls._get_stylized_words(words, style)
190
191 1
        return words_template[style] % "\n".join(items)
192
193 1
    @classmethod
194 1
    def __case_insensitive_words_filter(cls, name, words, partial_results):
195 1
        return words.filter(cls.name.ilike(f"{name}%")) \
196
            if partial_results else words.filter(cls.name.ilike(name))
197
198 1
    @classmethod
199 1
    def __case_sensitive_words_filter(cls, name, words, partial_results):
200 1
        return words.filter(cls.name.like(f"{name}%")) \
201
            if partial_results else words.filter(cls.name == name)
202
203 1
    @staticmethod
204 1
    def _get_stylized_words(
205
            words: list, style: str = DEFAULT_HTML_STYLE) -> List[str]:
206
        """
207
208
        Args:
209
            words:
210
            style:
211
212
        Returns:
213
214
        """
215 1
        word_template = {
216
            "normal": '<div class="word" wid="%s">\n'
217
                      '<div class="word_line"><span class="word_name">%s</span>,</div>\n'
218
                      '<div class="meanings">\n%s\n</div>\n</div>',
219
            "ultra": '<w wid="%s"><wl>%s,</wl>\n<ms>\n%s\n</ms>\n</w>',
220
        }
221 1
        grouped_words = groupby(words, lambda ent: ent.name)
222 1
        group_words = {k: list(g) for k, g in grouped_words}
223 1
        items = []
224 1
        for word_name, words_list in group_words.items():
225 1
            meanings = "\n".join([word.html_meaning(style) for word in words_list])
226 1
            items.append(word_template[style] % (word_name.lower(), word_name, meanings))
227 1
        return items
228
229 1
    def html_origin(self, style: str = DEFAULT_HTML_STYLE):
230
        """
231
232
        Args:
233
            style:
234
235
        Returns:
236
237
        """
238 1
        orig = self.origin
239 1
        orig_x = self.origin_x
240
241 1
        if not (orig or orig_x):
242 1
            return str()
243
244 1
        origin = self.__generate_origin(orig, orig_x)
245
246 1
        if style == "normal":
247 1
            return f'<span class="m_origin">&lt;{origin}&gt;</span> '
248 1
        return f'<o>&lt;{origin}&gt;</o> '
249
250 1
    @staticmethod
251 1
    def __generate_origin(orig: str, orig_x: str) -> str:
252
        """
253
        Generate basic 'origin' string
254
        Args:
255
            orig:
256
            orig_x:
257
258
        Returns:
259
260
        """
261 1
        if not orig_x:
262 1
            return orig
263
264 1
        if not orig:
265 1
            return orig_x
266
267 1
        return f'{orig}={orig_x}'
268
269 1
    def html_definitions(self, style: str = DEFAULT_HTML_STYLE):
270
        """
271
272
        :param style:
273
        :return:
274
        """
275 1
        return [HTMLExportDefinition.export_for_loglan(
276
            d, style=style) for d in list(self.definitions)]
277
278 1
    def meaning(self, style: str = DEFAULT_HTML_STYLE) -> dict:
279
        """
280
281
        :param style:
282
        :return:
283
        """
284 1
        html_affixes, html_match, html_rank,\
285
            html_source, html_type, html_used_in,\
286
            html_year, t_technical = self.get_styled_values(style)
287
288 1
        html_tech = t_technical % f'{html_match}{html_type}{html_source}{html_year}{html_rank}'
289 1
        html_tech = f'{html_affixes}{self.html_origin(style)}{html_tech}'
290
291 1
        return {
292
            "mid": self.id,
293
            "technical": html_tech,
294
            "definitions": self.html_definitions(style),
295
            "used_in": html_used_in
296
        }
297
298 1
    def get_styled_values(self, style: str = DEFAULT_HTML_STYLE) -> tuple:
299
        """
300
301
        Args:
302
            style:
303
304
        Returns:
305
306
        """
307 1
        tags = {
308
            "normal": [
309
                '<span class="m_afx">%s</span> ', '<span class="m_match">%s</span> ',
310
                '<span class="m_rank">%s</span>', '<span class="m_author">%s</span> ',
311
                '<span class="m_type">%s</span> ', '<span class="m_use">%s</span>',
312
                '<span class="m_year">%s</span> ', '<span class="m_technical">%s</span>'],
313
            "ultra": [
314
                '<afx>%s</afx> ', '%s ', '%s', '%s ', '%s ',
315
                '<use>%s</use>', '%s ', '<tec>%s</tec>'],
316
        }
317
318 1
        def _stringer(tag: str, value: Optional[str], default_value: Optional[str] = str()):
319 1
            return tag % value if value else default_value
320
321 1
        values = [self.e_affixes, self.match, self.rank, self.e_source, self.type.type,
322
                  self.e_usedin.replace("| ", "|&nbsp;"), self.e_year, None]
323 1
        default_values = [str(), str(), str(), str(), str(), None, str(), tags[style][-1]]
324
325 1
        return tuple([_stringer(tag, value, default_value) for tag, value, default_value in
326
                      zip(tags[style], values, default_values)])
327
328 1
    def html_meaning(self, style: str = DEFAULT_HTML_STYLE) -> str:
329
        """
330
331
        Args:
332
            style:
333
334
        Returns:
335
336
        """
337 1
        n_l = "\n"
338 1
        meaning_dict = self.meaning(style)
339 1
        if style == "normal":
340 1
            used_in_list = f'<div class="used_in">Used In: ' \
341
                           f'{meaning_dict.get("used_in")}</div>\n</div>' \
342
                if meaning_dict.get("used_in") else "</div>"
343 1
            return f'<div class="meaning" id="{meaning_dict.get("mid")}">\n' \
344
                   f'<div class="technical">{meaning_dict.get("technical")}</div>\n' \
345
                   f'<div class="definitions">{n_l}' \
346
                   f'{n_l.join(meaning_dict.get("definitions"))}\n</div>\n{used_in_list}'
347
348 1
        used_in_list = f'<us>Used In: {meaning_dict.get("used_in")}</us>\n</m>' \
349
            if meaning_dict.get("used_in") else '</m>'
350 1
        return f'<m>\n<t>{meaning_dict.get("technical")}</t>\n' \
351
               f'<ds>{n_l}' \
352
               f'{n_l.join(meaning_dict.get("definitions"))}\n</ds>\n{used_in_list}'
353
354 1
    @staticmethod
355 1
    def translation_by_key(
356
            key: str, language: str = None,
357
            style: str = DEFAULT_HTML_STYLE) -> Optional[str]:
358
        """
359
        Get information about loglan words by key in a foreign language
360
        Args:
361
            key:
362
            language:
363
            style:
364
365
        Returns:
366
367
        """
368
369 1
        words = HTMLExportWord.by_key(key, language).order_by(HTMLExportWord.name).all()
370
371 1
        if not words:
372 1
            return None
373
374 1
        result = HTMLExportWord.__definitions_by_key(key, words, style)
375
376 1
        new = '\n'
377
378 1
        return new.join([f"{new.join(definitions)}"
379
                         for _, definitions in result.items()]).strip()
380
381 1
    @staticmethod
382 1
    def __definitions_by_key(
383
            key: str, words: List[ExportWord], style: str = DEFAULT_HTML_STYLE) -> dict:
384
        """
385
386
        Args:
387
            key:
388
            words:
389
            style:
390
391
        Returns:
392
393
        """
394 1
        result = {}
395 1
        for word in words:
396 1
            result[word.name] = []
397 1
            definitions = [
398
                HTMLExportDefinition.export_for_english(d, word=key, style=style)
399
                for d in word.definitions if key.lower() in [key.word.lower() for key in d.keys]]
400 1
            result[word.name].extend(definitions)
401
        return result
402