protoparser.py 23.8 KB
Newer Older
Robin Sonnabend's avatar
Robin Sonnabend committed
1
import regex as re
Robin Sonnabend's avatar
Robin Sonnabend committed
2
import sys
Robin Sonnabend's avatar
Robin Sonnabend committed
3
from collections import OrderedDict
Robin Sonnabend's avatar
Robin Sonnabend committed
4
from enum import Enum
Robin Sonnabend's avatar
Robin Sonnabend committed
5

6
from shared import escape_tex
Robin Sonnabend's avatar
Robin Sonnabend committed
7
from utils import footnote_hash
8

9
from shared import config
Robin Sonnabend's avatar
Robin Sonnabend committed
10

11
12
INDENT_LETTER = "-"

Robin Sonnabend's avatar
Robin Sonnabend committed
13

Robin Sonnabend's avatar
Robin Sonnabend committed
14
class ParserException(Exception):
Robin Sonnabend's avatar
Robin Sonnabend committed
15
16
    name = "Parser Exception"
    has_explanation = False
Robin Sonnabend's avatar
Robin Sonnabend committed
17

18
    def __init__(self, message, linenumber=None, tree=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
19
20
        self.message = message
        self.linenumber = linenumber
21
        self.tree = tree
Robin Sonnabend's avatar
Robin Sonnabend committed
22

Robin Sonnabend's avatar
Robin Sonnabend committed
23
24
25
    def __str__(self):
        result = ""
        if self.linenumber is not None:
Robin Sonnabend's avatar
Robin Sonnabend committed
26
27
            result = "Exception at line {}: {}".format(
                self.linenumber, self.message)
Robin Sonnabend's avatar
Robin Sonnabend committed
28
29
30
31
32
33
        else:
            result = "Exception: {}".format(self.message)
        if self.has_explanation:
            result += "\n" + self.explanation
        return result

Robin Sonnabend's avatar
Robin Sonnabend committed
34

Robin Sonnabend's avatar
Robin Sonnabend committed
35
36
37
38
class RenderType(Enum):
    latex = 0
    wikitext = 1
    plaintext = 2
39
    html = 3
40
    dokuwiki = 4
Robin Sonnabend's avatar
Robin Sonnabend committed
41

Robin Sonnabend's avatar
Robin Sonnabend committed
42

Robin Sonnabend's avatar
Robin Sonnabend committed
43
def _not_implemented(self, render_type):
Robin Sonnabend's avatar
Robin Sonnabend committed
44
45
46
47
    return NotImplementedError(
        "The rendertype {} has not been implemented for {}.".format(
            render_type.name, self.__class__.__name__))

Robin Sonnabend's avatar
Robin Sonnabend committed
48

Robin Sonnabend's avatar
Robin Sonnabend committed
49
50
51
52
53
class Element:
    """
    Generic (abstract) base element. Should never really exist.
    Template for what an element class should contain.
    """
Robin Sonnabend's avatar
Robin Sonnabend committed
54
    def render(self, render_type, show_private, level=None, protocol=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
55
56
57
58
59
60
61
62
63
64
        """
        Renders the element to TeX.
        Returns:
        - a TeX-representation of the element
        """
        return "Generic Base Syntax Element, this is not supposed to appear."

    def dump(self, level=None):
        if level is None:
            level = 0
65
        return "{}element".format(INDENT_LETTER * level)
Robin Sonnabend's avatar
Robin Sonnabend committed
66
67
68
69
70
71
72

    @staticmethod
    def parse(match, current, linenumber=None):
        """
        Parses a match of this elements pattern.
        Arguments:
        - match: the match of this elements pattern
Robin Sonnabend's avatar
Robin Sonnabend committed
73
74
        - current: the current element of the document. Should be a fork.
            May be modified.
Robin Sonnabend's avatar
Robin Sonnabend committed
75
76
77
78
79
        - linenumber: the current line number, for error messages
        Returns:
        - the new current element
        - the line number after parsing this element
        """
Robin Sonnabend's avatar
Robin Sonnabend committed
80
81
        raise ParserException(
            "Trying to parse the generic base element!", linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

    @staticmethod
    def parse_inner(match, current, linenumber=None):
        """
        Do the parsing for every element. Checks if the match exists.
        Arguments:
        - match: the match of this elements pattern
        - current = the current element of the document. Should be a fork.
        - linenumber: the current line number, for error messages
        Returns:
        - new line number
        """
        if match is None:
            raise ParserException("Source does not match!", linenumber)
        length = match.group().count("\n")
97
        return length + (0 if linenumber is None else linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
98
99
100
101
102
103
104
105
106
107
108
109
110
111

    @staticmethod
    def parse_outer(element, current):
        """
        Handle the insertion of the object into the tree.
        Arguments:
        - element: the new parsed element to insert
        - current: the current element of the parsed document
        Returns:
        - the new current element
        """
        current.append(element)
        if isinstance(element, Fork):
            return element
Robin Sonnabend's avatar
Robin Sonnabend committed
112
113
114
        else:
            element.fork = current
            return current
Robin Sonnabend's avatar
Robin Sonnabend committed
115

Robin Sonnabend's avatar
Robin Sonnabend committed
116
117
    PATTERN = r"x(?<!x)"

Robin Sonnabend's avatar
Robin Sonnabend committed
118
119

class Content(Element):
120
    def __init__(self, children, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
121
        self.children = children
122
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
123

Robin Sonnabend's avatar
Robin Sonnabend committed
124
    def render(self, render_type, show_private, level=None, protocol=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
125
126
127
        return "".join(map(lambda e: e.render(
            render_type, show_private, level=level, protocol=protocol),
            self.children))
Robin Sonnabend's avatar
Robin Sonnabend committed
128
129
130
131

    def dump(self, level=None):
        if level is None:
            level = 0
132
        result_lines = ["{}content:".format(INDENT_LETTER * level)]
Robin Sonnabend's avatar
Robin Sonnabend committed
133
        for child in self.children:
134
135
            result_lines.append(child.dump(level + 1))
        return "\n".join(result_lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
136

137
    def get_tags(self, tags):
Robin Sonnabend's avatar
Robin Sonnabend committed
138
139
140
141
        tags.extend([
            child for child in self.children
            if isinstance(child, Tag)
        ])
142
143
        return tags

Robin Sonnabend's avatar
Robin Sonnabend committed
144
145
146
147
    @staticmethod
    def parse(match, current, linenumber=None):
        linenumber = Element.parse_inner(match, current, linenumber)
        if match.group("content") is None:
Robin Sonnabend's avatar
Robin Sonnabend committed
148
149
            raise ParserException(
                "Content is missing its content!", linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
150
        content = match.group("content")
Robin Sonnabend's avatar
Robin Sonnabend committed
151
        element = Content.from_content(content, current, linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
152
153
154
155
156
157
        if len(content) == 0:
            return current, linenumber
        current = Element.parse_outer(element, current)
        return current, linenumber

    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
158
    def from_content(content, current, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
159
160
161
162
163
164
165
        children = []
        while len(content) > 0:
            matched = False
            for pattern in TEXT_PATTERNS:
                match = pattern.match(content)
                if match is not None:
                    matched = True
Robin Sonnabend's avatar
Robin Sonnabend committed
166
167
                    children.append(TEXT_PATTERNS[pattern](
                        match, current, linenumber))
Robin Sonnabend's avatar
Robin Sonnabend committed
168
169
170
                    content = content[len(match.group()):]
                    break
            if not matched:
Robin Sonnabend's avatar
Robin Sonnabend committed
171
172
173
174
175
                raise ParserException(
                    "Dies ist kein valider Tag! "
                    "(mögliche Tags sind: {})".format(
                        ", ".join(Tag.KNOWN_TAGS)),
                    linenumber)
176
        return Content(children, linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
177

Robin Sonnabend's avatar
Robin Sonnabend committed
178
179
180
181
    PATTERN = (
        r"\s*(?<content>(?:(?:[^\[\];\r\n{}]+)|(?:[^\[\];\r\n{}]+)?"
        r"(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)+));?")

Robin Sonnabend's avatar
Robin Sonnabend committed
182
183

class Text:
Robin Sonnabend's avatar
Robin Sonnabend committed
184
    def __init__(self, text, linenumber, fork):
Robin Sonnabend's avatar
Robin Sonnabend committed
185
        self.text = text
186
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
187
        self.fork = fork
Robin Sonnabend's avatar
Robin Sonnabend committed
188

Robin Sonnabend's avatar
Robin Sonnabend committed
189
190
191
192
193
    def render(self, render_type, show_private, level=None, protocol=None):
        if render_type == RenderType.latex:
            return escape_tex(self.text)
        elif render_type == RenderType.wikitext:
            return self.text
194
        elif render_type == RenderType.plaintext:
Robin Sonnabend's avatar
Robin Sonnabend committed
195
            return self.text
196
197
        elif render_type == RenderType.html:
            return self.text
198
199
        elif render_type == RenderType.dokuwiki:
            return self.text
Robin Sonnabend's avatar
Robin Sonnabend committed
200
201
        else:
            raise _not_implemented(self, render_type)
Robin Sonnabend's avatar
Robin Sonnabend committed
202
203
204
205

    def dump(self, level=None):
        if level is None:
            level = 0
206
        return "{}text: {}".format(INDENT_LETTER * level, self.text)
Robin Sonnabend's avatar
Robin Sonnabend committed
207
208

    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
209
    def parse(match, current, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
210
211
212
213
214
        if match is None:
            raise ParserException("Text is not actually a text!", linenumber)
        content = match.group("text")
        if content is None:
            raise ParserException("Text is empty!", linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
215
        return Text(content, linenumber, current)
Robin Sonnabend's avatar
Robin Sonnabend committed
216

217
    PATTERN = r"(?<text>\[?[^\[{}]+)(?:(?=\[)|$)"
Robin Sonnabend's avatar
Robin Sonnabend committed
218
219
220


class Tag:
Robin Sonnabend's avatar
Robin Sonnabend committed
221
    def __init__(self, name, values, linenumber, fork):
Robin Sonnabend's avatar
Robin Sonnabend committed
222
223
        self.name = name
        self.values = values
224
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
225
        self.fork = fork
Robin Sonnabend's avatar
Robin Sonnabend committed
226

Robin Sonnabend's avatar
Robin Sonnabend committed
227
228
229
230
231
    def render(self, render_type, show_private, level=None, protocol=None):
        if render_type == RenderType.latex:
            if self.name == "url":
                return r"\url{{{}}}".format(self.values[0])
            elif self.name == "todo":
Robin Sonnabend's avatar
Robin Sonnabend committed
232
233
                if not show_private:
                    return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
234
                return self.todo.render_latex(current_protocol=protocol)
235
            elif self.name == "beschluss":
236
                if len(self.decision.categories):
Robin Sonnabend's avatar
Robin Sonnabend committed
237
238
239
                    return r"\Beschluss[{}]{{{}}}".format(
                        escape_tex(self.decision.get_categories_str()),
                        escape_tex(self.decision.content))
240
241
                else:
                    return r"\Beschluss{{{}}}".format(self.decision.content)
Robin Sonnabend's avatar
Robin Sonnabend committed
242
243
            elif self.name == "footnote":
                return r"\footnote{{{}}}".format(self.values[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
244
245
246
            return r"\textbf{{{}:}} {}".format(
                escape_tex(self.name.capitalize()),
                escape_tex(";".join(self.values)))
247
        elif render_type == RenderType.plaintext:
Robin Sonnabend's avatar
Robin Sonnabend committed
248
249
            if self.name == "url":
                return self.values[0]
Robin Sonnabend's avatar
Robin Sonnabend committed
250
251
252
253
            elif self.name == "todo":
                if not show_private:
                    return ""
                return self.values[0]
Robin Sonnabend's avatar
Robin Sonnabend committed
254
255
            elif self.name == "footnote":
                return "[^]({})".format(self.values[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
256
257
            return "{}: {}".format(
                self.name.capitalize(), ";".join(self.values))
Robin Sonnabend's avatar
Robin Sonnabend committed
258
259
260
261
        elif render_type == RenderType.wikitext:
            if self.name == "url":
                return "[{0} {0}]".format(self.values[0])
            elif self.name == "todo":
Robin Sonnabend's avatar
Robin Sonnabend committed
262
263
                if not show_private:
                    return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
264
                return self.todo.render_wikitext(current_protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
265
266
            elif self.name == "footnote":
                return "<ref>{}</ref>".format(self.values[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
267
268
            return "'''{}:''' {}".format(
                self.name.capitalize(), ";".join(self.values))
269
270
271
272
273
274
275
276
277
278
279
280
281
        elif render_type == RenderType.html:
            if self.name == "url":
                return "<a href=\"{0}\">{0}</a>".format(self.values[0])
            elif self.name == "todo":
                if not show_private:
                    return ""
                if getattr(self, "todo", None) is not None:
                    return self.todo.render_html(current_protocol=protocol)
                else:
                    return "<b>Todo:</b> {}".format(";".join(self.values))
            elif self.name == "beschluss":
                if getattr(self, "decision", None) is not None:
                    parts = ["<b>Beschluss:</b>", self.decision.content]
282
283
284
                    if len(self.decision.categories) > 0:
                        parts.append("<i>{}</i>".format(
                            self.decision.get_categories_str()))
285
286
287
                    return " ".join(parts)
                else:
                    return "<b>Beschluss:</b> {}".format(self.values[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
288
            elif self.name == "footnote":
Robin Sonnabend's avatar
Robin Sonnabend committed
289
290
291
                return (
                    '<sup id="#fnref{0}"><a href="#fn{0}">Fn</a></sup>'.format(
                        footnote_hash(self.values[0])))
292
            return "[{}: {}]".format(self.name, ";".join(self.values))
293
294
295
296
297
298
        elif render_type == RenderType.dokuwiki:
            if self.name == "url":
                return self.values[0]
            elif self.name == "todo":
                if not show_private:
                    return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
299
300
                return self.todo.render_wikitext(
                    current_protocol=protocol, use_dokuwiki=True)
301
            elif self.name == "beschluss":
Robin Sonnabend's avatar
Robin Sonnabend committed
302
303
                return "**{}:** {}".format(
                    self.name.capitalize(), ";".join(self.values))
304
305
306
            elif self.name == "footnote":
                return "(({}))".format(self.values[0])
            else:
Robin Sonnabend's avatar
Robin Sonnabend committed
307
308
                return "**{}:** {}".format(
                    self.name.capitalize(), ";".join(self.values))
Robin Sonnabend's avatar
Robin Sonnabend committed
309
310
        else:
            raise _not_implemented(self, render_type)
Robin Sonnabend's avatar
Robin Sonnabend committed
311
312
313
314

    def dump(self, level=None):
        if level is None:
            level = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
315
316
        return "{}tag: {}: {}".format(
            INDENT_LETTER * level, self.name, "; ".join(self.values))
Robin Sonnabend's avatar
Robin Sonnabend committed
317
318

    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
319
    def parse(match, current, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
320
321
322
323
324
325
        if match is None:
            raise ParserException("Tag is not actually a tag!", linenumber)
        content = match.group("content")
        if content is None:
            raise ParserException("Tag is empty!", linenumber)
        parts = content.split(";")
Robin Sonnabend's avatar
Robin Sonnabend committed
326
        return Tag(parts[0], parts[1:], linenumber, current)
Robin Sonnabend's avatar
Robin Sonnabend committed
327

328
329
    PATTERN = r"\[(?<content>[^\]]*)\]"

330
    KNOWN_TAGS = ["todo", "url", "beschluss", "footnote", "sitzung"]
Robin Sonnabend's avatar
Robin Sonnabend committed
331
332
333


class Empty(Element):
334
335
    def __init__(self, linenumber):
        linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
336

Robin Sonnabend's avatar
Robin Sonnabend committed
337
    def render(self, render_type, show_private, level=None, protocol=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
338
339
340
341
342
        return ""

    def dump(self, level=None):
        if level is None:
            level = 0
343
        return "{}empty".format(INDENT_LETTER * level)
Robin Sonnabend's avatar
Robin Sonnabend committed
344
345
346
347
348
349

    @staticmethod
    def parse(match, current, linenumber=None):
        linenumber = Element.parse_inner(match, current, linenumber)
        return current, linenumber

350
    PATTERN = r"(?:\s+|;)"
Robin Sonnabend's avatar
Robin Sonnabend committed
351

Robin Sonnabend's avatar
Robin Sonnabend committed
352

Robin Sonnabend's avatar
Robin Sonnabend committed
353
class Remark(Element):
354
    def __init__(self, name, value, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
355
356
        self.name = name
        self.value = value
357
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
358

Robin Sonnabend's avatar
Robin Sonnabend committed
359
360
361
362
363
364
365
    def render(self, render_type, show_private, level=None, protocol=None):
        if render_type == RenderType.latex:
            return r"\textbf{{{}}}: {}".format(self.name, self.value)
        elif render_type == RenderType.wikitext:
            return "{}: {}".format(self.name, self.value)
        elif render_type == RenderType.plaintext:
            return "{}: {}".format(RenderType.plaintex)
366
367
        elif render_type == RenderType.html:
            return "<p>{}: {}</p>".format(self.name, self.value)
368
369
        elif render_type == RenderType.dokuwiki:
            return r"{}: {}\\".format(self.name, self.value)
370
371
        else:
            raise _not_implemented(self, render_type)
Robin Sonnabend's avatar
Robin Sonnabend committed
372
373
374
375

    def dump(self, level=None):
        if level is None:
            level = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
376
377
        return "{}remark: {}: {}".format(
            INDENT_LETTER * level, self.name, self.value)
Robin Sonnabend's avatar
Robin Sonnabend committed
378

Robin Sonnabend's avatar
Robin Sonnabend committed
379
380
381
    def get_tags(self, tags):
        return tags

Robin Sonnabend's avatar
Robin Sonnabend committed
382
383
384
385
386
387
388
389
390
391
    @staticmethod
    def parse(match, current, linenumber=None):
        linenumber = Element.parse_inner(match, current, linenumber)
        if match.group("content") is None:
            raise ParserException("Remark is missing its content!", linenumber)
        content = match.group("content")
        parts = content.split(";", 1)
        if len(parts) < 2:
            raise ParserException("Remark value is empty!", linenumber)
        name, value = parts
392
        element = Remark(name, value, linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
393
394
395
396
397
        current = Element.parse_outer(element, current)
        return current, linenumber

    PATTERN = r"\s*\#(?<content>[^\n]+)"

Robin Sonnabend's avatar
Robin Sonnabend committed
398

Robin Sonnabend's avatar
Robin Sonnabend committed
399
class Fork(Element):
400
401
    def __init__(self, is_top, name, parent, linenumber, children=None):
        self.is_top = is_top
Robin Sonnabend's avatar
Robin Sonnabend committed
402
        self.name = name.strip() if name else None
Robin Sonnabend's avatar
Robin Sonnabend committed
403
        self.parent = parent
404
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
405
406
407
408
409
        self.children = [] if children is None else children

    def dump(self, level=None):
        if level is None:
            level = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
410
411
412
413
414
415
        result_lines = [
            "{}fork: {}'{}'".format(
                INDENT_LETTER * level,
                "TOP " if self.is_top else "",
                self.name)
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
416
        for child in self.children:
417
418
            result_lines.append(child.dump(level + 1))
        return "\n".join(result_lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
419

Robin Sonnabend's avatar
Robin Sonnabend committed
420
    def test_private(self, name):
421
422
        if name is None:
            return False
Robin Sonnabend's avatar
Robin Sonnabend committed
423
        stripped_name = name.replace(":", "").strip()
Robin Sonnabend's avatar
Robin Sonnabend committed
424
        return stripped_name in config.PRIVATE_KEYWORDS
Robin Sonnabend's avatar
Robin Sonnabend committed
425

Robin Sonnabend's avatar
Robin Sonnabend committed
426
    def render(self, render_type, show_private, level, protocol=None):
427
        name_line = self.name if self.name is not None else ""
Robin Sonnabend's avatar
Robin Sonnabend committed
428
429
        if level == 0 and self.name == "Todos" and not show_private:
            return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
430
431
432
433
434
        if render_type == RenderType.latex:
            begin_line = r"\begin{itemize}"
            end_line = r"\end{itemize}"
            content_parts = []
            for child in self.children:
Robin Sonnabend's avatar
Robin Sonnabend committed
435
436
437
                part = child.render(
                    render_type, show_private, level=level + 1,
                    protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
438
439
440
441
442
443
                if len(part.strip()) == 0:
                    continue
                if not part.startswith(r"\item"):
                    part = r"\item {}".format(part)
                content_parts.append(part)
            content_lines = "\n".join(content_parts)
444
445
            if len(content_lines.strip()) == 0:
                content_lines = "\\item Nichts\n"
Robin Sonnabend's avatar
Robin Sonnabend committed
446
447
448
449
            if level == 0:
                return "\n".join([begin_line, content_lines, end_line])
            elif self.test_private(self.name):
                if show_private:
Robin Sonnabend's avatar
Robin Sonnabend committed
450
451
                    return (r"\begin{tcolorbox}[breakable,title=Interner "
                            r"Abschnitt]" + "\n"
452
453
                            + r"\begin{itemize}" + "\n"
                            + content_lines + "\n"
454
455
                            + r"\end{itemize}" + "\n"
                            + r"\end{tcolorbox}")
Robin Sonnabend's avatar
Robin Sonnabend committed
456
                else:
Robin Sonnabend's avatar
Robin Sonnabend committed
457
458
                    return (r"\textit{[An dieser Stelle wurde intern "
                            r"protokolliert.]}")
Robin Sonnabend's avatar
Robin Sonnabend committed
459
            else:
Robin Sonnabend's avatar
Robin Sonnabend committed
460
461
462
463
464
465
                return "\n".join([
                    escape_tex(name_line), begin_line,
                    content_lines, end_line
                ])
        elif (render_type == RenderType.wikitext
                or render_type == RenderType.dokuwiki):
Robin Sonnabend's avatar
Robin Sonnabend committed
466
467
            equal_signs = level + 2
            if render_type == RenderType.dokuwiki:
Robin Sonnabend's avatar
Robin Sonnabend committed
468
                equal_signs = 5 - level
Robin Sonnabend's avatar
Robin Sonnabend committed
469
            title_line = "{0} {1} {0}".format("=" * equal_signs, name_line)
Robin Sonnabend's avatar
Robin Sonnabend committed
470
471
            content_parts = []
            for child in self.children:
Robin Sonnabend's avatar
Robin Sonnabend committed
472
473
474
                part = child.render(
                    render_type, show_private, level=level + 1,
                    protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
475
476
477
                if len(part.strip()) == 0:
                    continue
                content_parts.append(part)
Robin Sonnabend's avatar
Robin Sonnabend committed
478
479
            content_lines = "{}\n\n{}\n".format(
                title_line, "\n\n".join(content_parts))
Robin Sonnabend's avatar
Robin Sonnabend committed
480
            if self.test_private(self.name) and not show_private:
Robin Sonnabend's avatar
Robin Sonnabend committed
481
                return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
482
483
484
485
486
487
            else:
                return content_lines
        elif render_type == RenderType.plaintext:
            title_line = "{} {}".format("#" * (level + 1), name_line)
            content_parts = []
            for child in self.children:
Robin Sonnabend's avatar
Robin Sonnabend committed
488
489
490
                part = child.render(
                    render_type, show_private, level=level + 1,
                    protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
491
492
493
                if len(part.strip()) == 0:
                    continue
                content_parts.append(part)
Robin Sonnabend's avatar
Robin Sonnabend committed
494
495
            content_lines = "{}\n{}".format(
                title_line, "\n".join(content_parts))
Robin Sonnabend's avatar
Robin Sonnabend committed
496
497
498
499
            if self.test_private(self.name) and not show_private:
                return ""
            else:
                return content_lines
500
        elif render_type == RenderType.html:
501
502
503
            depth = level + 1 + getattr(config, "HTML_LEVEL_OFFSET", 0)
            content_lines = ""
            if depth < 5:
Robin Sonnabend's avatar
Robin Sonnabend committed
504
505
                title_line = "<h{depth}>{content}</h{depth}>".format(
                    depth=depth, content=name_line)
506
507
                content_parts = []
                for child in self.children:
Robin Sonnabend's avatar
Robin Sonnabend committed
508
509
510
                    part = child.render(
                        render_type, show_private, level=level + 1,
                        protocol=protocol)
511
512
513
                    if len(part.strip()) == 0:
                        continue
                    content_parts.append("<p>{}</p>".format(part))
Robin Sonnabend's avatar
Robin Sonnabend committed
514
515
                content_lines = "{}\n\n{}".format(
                    title_line, "\n".join(content_parts))
516
517
518
            else:
                content_parts = []
                for child in self.children:
Robin Sonnabend's avatar
Robin Sonnabend committed
519
520
521
                    part = child.render(
                        render_type, show_private, level=level + 1,
                        protocol=protocol)
522
523
524
                    if len(part.strip()) == 0:
                        continue
                    content_parts.append("<li>{}</li>".format(part))
Robin Sonnabend's avatar
Robin Sonnabend committed
525
526
                content_lines = "{}\n<ul>\n{}\n</ul>".format(
                    name_line, "\n".join(content_parts))
527
528
529
530
            if self.test_private(self.name) and not show_private:
                return ""
            else:
                return content_lines
Robin Sonnabend's avatar
Robin Sonnabend committed
531
        else:
Robin Sonnabend's avatar
Robin Sonnabend committed
532
533
            raise _not_implemented(self, render_type)

534
535
536
537
538
539
540
    def get_tags(self, tags=None):
        if tags is None:
            tags = []
        for child in self.children:
            child.get_tags(tags)
        return tags

Robin Sonnabend's avatar
Robin Sonnabend committed
541
    def is_anonymous(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
542
        return self.name is None
Robin Sonnabend's avatar
Robin Sonnabend committed
543
544
545
546

    def is_root(self):
        return self.parent is None

Robin Sonnabend's avatar
Robin Sonnabend committed
547
548
549
550
551
    def get_top(self):
        if self.is_root() or self.parent.is_root():
            return self
        return self.parent.get_top()

552
553
554
555
    def get_top_number(self):
        if self.is_root():
            return 1
        top = self.get_top()
Robin Sonnabend's avatar
Robin Sonnabend committed
556
557
        tops = [
            child
558
559
560
561
562
            for child in top.parent.children
            if isinstance(child, Fork)
        ]
        return tops.index(top) + 1

563
564
565
566
567
568
569
570
571
572
573
    def get_maxdepth(self):
        child_depths = [
            child.get_maxdepth()
            for child in self.children
            if isinstance(child, Fork)
        ]
        if len(child_depths) > 0:
            return max(child_depths) + 1
        else:
            return 1

Robin Sonnabend's avatar
Robin Sonnabend committed
574
575
576
577
578
579
    def get_visible_elements(self, show_private, elements=None):
        if elements is None:
            elements = set()
        if show_private or not self.test_private(self.name):
            for child in self.children:
                elements.add(child)
580
581
582
                if isinstance(child, Content):
                    elements.update(child.children)
                elif isinstance(child, Fork):
Robin Sonnabend's avatar
Robin Sonnabend committed
583
584
585
                    child.get_visible_elements(show_private, elements)
        return elements

Robin Sonnabend's avatar
Robin Sonnabend committed
586
587
    @staticmethod
    def create_root():
588
        return Fork(None, None, None, 0)
Robin Sonnabend's avatar
Robin Sonnabend committed
589
590
591
592

    @staticmethod
    def parse(match, current, linenumber=None):
        linenumber = Element.parse_inner(match, current, linenumber)
593
594
595
596
597
598
599
        topname = match.group("topname")
        name = match.group("name")
        is_top = False
        if topname is not None:
            is_top = True
            name = topname
        element = Fork(is_top, name, current, linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
600
601
602
603
604
605
606
        current = Element.parse_outer(element, current)
        return current, linenumber

    @staticmethod
    def parse_end(match, current, linenumber=None):
        linenumber = Element.parse_inner(match, current, linenumber)
        if current.is_root():
Robin Sonnabend's avatar
Robin Sonnabend committed
607
608
            raise ParserException(
                "Found end tag for root element!", linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
609
610
611
612
613
614
        current = current.parent
        return current, linenumber

    def append(self, element):
        self.children.append(element)

Robin Sonnabend's avatar
Robin Sonnabend committed
615
616
    PATTERN = (
        r"\s*(?<name>(?:[^{};\n])+)?\n?\s*{(?:TOP\h*(?<topname>[^;{}\n]+))?")
Robin Sonnabend's avatar
Robin Sonnabend committed
617
618
    END_PATTERN = r"\s*};?"

Robin Sonnabend's avatar
Robin Sonnabend committed
619

Robin Sonnabend's avatar
Robin Sonnabend committed
620
621
622
623
624
625
626
627
628
PATTERNS = OrderedDict([
    (re.compile(Fork.PATTERN), Fork.parse),
    (re.compile(Fork.END_PATTERN), Fork.parse_end),
    (re.compile(Remark.PATTERN), Remark.parse),
    (re.compile(Content.PATTERN), Content.parse),
    (re.compile(Empty.PATTERN), Empty.parse)
])

TEXT_PATTERNS = OrderedDict([
629
630
    (re.compile(Tag.PATTERN), Tag.parse),
    (re.compile(Text.PATTERN), Text.parse)
Robin Sonnabend's avatar
Robin Sonnabend committed
631
632
])

Robin Sonnabend's avatar
Robin Sonnabend committed
633

Robin Sonnabend's avatar
Robin Sonnabend committed
634
635
636
637
638
639
640
641
642
643
def parse(source):
    linenumber = 1
    tree = Fork.create_root()
    current = tree
    while len(source) > 0:
        found = False
        for pattern in PATTERNS:
            match = pattern.match(source)
            if match is not None:
                source = source[len(match.group()):]
644
                try:
Robin Sonnabend's avatar
Robin Sonnabend committed
645
646
                    current, linenumber = PATTERNS[pattern](
                        match, current, linenumber)
647
648
649
                except ParserException as exc:
                    exc.tree = tree
                    raise exc
Robin Sonnabend's avatar
Robin Sonnabend committed
650
651
652
                found = True
                break
        if not found:
Robin Sonnabend's avatar
Robin Sonnabend committed
653
654
            raise ParserException(
                "No matching syntax element found!", linenumber, tree=tree)
Robin Sonnabend's avatar
Robin Sonnabend committed
655
    if current is not tree:
Robin Sonnabend's avatar
Robin Sonnabend committed
656
657
658
659
        raise ParserException(
            "Du hast vergessen, Klammern zu schließen! (die öffnende ist in "
            "Zeile {})".format(
                current.linenumber), linenumber=current.linenumber, tree=tree)
Robin Sonnabend's avatar
Robin Sonnabend committed
660
661
    return tree

Robin Sonnabend's avatar
Robin Sonnabend committed
662

Robin Sonnabend's avatar
Robin Sonnabend committed
663
def main(test_file_name=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
664
    source = ""
Robin Sonnabend's avatar
Robin Sonnabend committed
665
666
    test_file_name = test_file_name or "source0"
    with open("test/{}.txt".format(test_file_name)) as f:
Robin Sonnabend's avatar
Robin Sonnabend committed
667
        source = f.read()
Robin Sonnabend's avatar
Robin Sonnabend committed
668
669
    try:
        tree = parse(source)
670
        print(tree.dump())
Robin Sonnabend's avatar
Robin Sonnabend committed
671
672
673
674
    except ParserException as e:
        print(e)
    else:
        print("worked!")
Robin Sonnabend's avatar
Robin Sonnabend committed
675

Robin Sonnabend's avatar
Robin Sonnabend committed
676
677

if __name__ == "__main__":
Robin Sonnabend's avatar
Robin Sonnabend committed
678
679
    test_file_name = sys.argv[1] if len(sys.argv) > 1 else None
    exit(main(test_file_name))