protoparser.tex 23.4 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

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

11
12
INDENT_LETTER = "-"

Robin Sonnabend's avatar
Robin Sonnabend committed
13
class ParserException(Exception):
Robin Sonnabend's avatar
Robin Sonnabend committed
14
15
16
    name = "Parser Exception"
    has_explanation = False
    #explanation = "The source did generally not match the expected protocol syntax."
17
    def __init__(self, message, linenumber=None, tree=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
18
19
        self.message = message
        self.linenumber = linenumber
20
        self.tree = tree
Robin Sonnabend's avatar
Robin Sonnabend committed
21

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

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

def _not_implemented(self, render_type):
    return NotImplementedError("The rendertype {} has not been implemented for {}.".format(render_type.name, self.__class__.__name__))

Robin Sonnabend's avatar
Robin Sonnabend committed
41
42
43
44
45
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
46
    def render(self, render_type, show_private, level=None, protocol=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
47
48
49
50
51
52
53
54
55
56
        """
        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
57
        return "{}element".format(INDENT_LETTER * level)
Robin Sonnabend's avatar
Robin Sonnabend committed
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

    @staticmethod
    def parse(match, current, linenumber=None):
        """
        Parses a match of this elements pattern.
        Arguments:
        - match: the match of this elements pattern
        - current: the current element of the document. Should be a fork. May be modified.
        - linenumber: the current line number, for error messages
        Returns:
        - the new current element
        - the line number after parsing this element
        """
        raise ParserException("Trying to parse the generic base element!", linenumber)

    @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")
87
        return length + (0 if linenumber is None else linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
88
89
90
91
92
93
94
95
96
97
98
99
100
101

    @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
102
103
104
        else:
            element.fork = current
            return current
Robin Sonnabend's avatar
Robin Sonnabend committed
105

Robin Sonnabend's avatar
Robin Sonnabend committed
106
    PATTERN = r"x(?<!x)" # yes, a master piece, but it should never be called
Robin Sonnabend's avatar
Robin Sonnabend committed
107
108

class Content(Element):
109
    def __init__(self, children, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
110
        self.children = children
111
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
112

Robin Sonnabend's avatar
Robin Sonnabend committed
113
114
    def render(self, render_type, show_private, level=None, protocol=None):
        return "".join(map(lambda e: e.render(render_type, show_private, level=level, protocol=protocol), self.children))
Robin Sonnabend's avatar
Robin Sonnabend committed
115
116
117
118

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

124
125
126
127
    def get_tags(self, tags):
        tags.extend([child for child in self.children if isinstance(child, Tag)])
        return tags

Robin Sonnabend's avatar
Robin Sonnabend committed
128
129
130
131
132
133
    @staticmethod
    def parse(match, current, linenumber=None):
        linenumber = Element.parse_inner(match, current, linenumber)
        if match.group("content") is None:
            raise ParserException("Content is missing its content!", linenumber)
        content = match.group("content")
Robin Sonnabend's avatar
Robin Sonnabend committed
134
        element = Content.from_content(content, current, linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
135
136
137
138
139
140
        if len(content) == 0:
            return current, linenumber
        current = Element.parse_outer(element, current)
        return current, linenumber

    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
141
    def from_content(content, current, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
142
143
144
145
146
147
148
        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
149
                    children.append(TEXT_PATTERNS[pattern](match, current, linenumber))
Robin Sonnabend's avatar
Robin Sonnabend committed
150
151
152
                    content = content[len(match.group()):]
                    break
            if not matched:
153
                raise ParserException("Dies ist kein valider Tag! (mögliche Tags sind: {})", linenumber, ", ".join(Tag.KNOWN_TAGS))
154
        return Content(children, linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
155

Robin Sonnabend's avatar
Robin Sonnabend committed
156
157
158
    # v1: has problems with missing semicolons
    #PATTERN = r"\s*(?<content>(?:[^\[\];]+)?(?:\[[^\]]+\][^;\[\]]*)*);"
    # v2: does not require the semicolon, but the newline
159
160
    #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?"
    # v3: does not allow braces in the content
161
162
    #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)*);?"
    # v4: do not allow empty match (require either the first or the second part to be non-empty)
163
    PATTERN = r"\s*(?<content>(?:(?:[^\[\];\r\n{}]+)|(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)+));?"
Administrator's avatar
Administrator committed
164
    # v5: do match emptystring if followed by a semi colon
165
    #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+);?|(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)+;?|;)"
Robin Sonnabend's avatar
Robin Sonnabend committed
166
167

class Text:
Robin Sonnabend's avatar
Robin Sonnabend committed
168
    def __init__(self, text, linenumber, fork):
Robin Sonnabend's avatar
Robin Sonnabend committed
169
        self.text = text
170
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
171
        self.fork = fork
Robin Sonnabend's avatar
Robin Sonnabend committed
172

Robin Sonnabend's avatar
Robin Sonnabend committed
173
174
175
176
177
    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
178
        elif render_type == RenderType.plaintext:
Robin Sonnabend's avatar
Robin Sonnabend committed
179
            return self.text
180
181
        elif render_type == RenderType.html:
            return self.text
Robin Sonnabend's avatar
Robin Sonnabend committed
182
183
        else:
            raise _not_implemented(self, render_type)
Robin Sonnabend's avatar
Robin Sonnabend committed
184
185
186
187

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

    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
191
    def parse(match, current, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
192
193
194
195
196
        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
197
        return Text(content, linenumber, current)
Robin Sonnabend's avatar
Robin Sonnabend committed
198

199
200
201
202
    # v1: does not allow any [, as that is part of a tag
    # PATTERN = r"(?<text>[^\[]+)(?:(?=\[)|$)"
    # v2: does allow one [ at the beginning, which is used if it did not match a tag
    PATTERN = r"(?<text>\[?[^\[{}]+)(?:(?=\[)|$)"
Robin Sonnabend's avatar
Robin Sonnabend committed
203
204
205


class Tag:
Robin Sonnabend's avatar
Robin Sonnabend committed
206
    def __init__(self, name, values, linenumber, fork):
Robin Sonnabend's avatar
Robin Sonnabend committed
207
208
        self.name = name
        self.values = values
209
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
210
        self.fork = fork
Robin Sonnabend's avatar
Robin Sonnabend committed
211

Robin Sonnabend's avatar
Robin Sonnabend committed
212
213
214
215
216
    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
217
218
                if not show_private:
                    return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
219
                return self.todo.render_latex(current_protocol=protocol)
220
            elif self.name == "beschluss":
221
222
223
224
225
226
                parts = [r"\textbf{{Beschluss:}} {}".format(self.decision.content)]
                if len(self.decision.categories):
                    parts.append(
                        r"\textit{{({})}}".format(self.decision.get_categories_str())
                    )
                return " ".join(parts)
Robin Sonnabend's avatar
Robin Sonnabend committed
227
228
            elif self.name == "footnote":
                return r"\footnote{{{}}}".format(self.values[0])
229
            return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(";".join(self.values)))
230
        elif render_type == RenderType.plaintext:
Robin Sonnabend's avatar
Robin Sonnabend committed
231
232
            if self.name == "url":
                return self.values[0]
Robin Sonnabend's avatar
Robin Sonnabend committed
233
234
235
236
            elif self.name == "todo":
                if not show_private:
                    return ""
                return self.values[0]
Robin Sonnabend's avatar
Robin Sonnabend committed
237
238
            elif self.name == "footnote":
                return "[^]({})".format(self.values[0])
239
            return "{}: {}".format(self.name.capitalize(), ";".join(self.values))
Robin Sonnabend's avatar
Robin Sonnabend committed
240
241
242
243
        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
244
245
                if not show_private:
                    return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
246
                return self.todo.render_wikitext(current_protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
247
248
            elif self.name == "footnote":
                return "<ref>{}</ref>".format(self.values[0])
249
            return "'''{}:''' {}".format(self.name.capitalize(), ";".join(self.values))
250
251
252
253
254
255
256
257
258
259
260
261
262
        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]
263
264
265
                    if len(self.decision.categories) > 0:
                        parts.append("<i>{}</i>".format(
                            self.decision.get_categories_str()))
266
267
268
                    return " ".join(parts)
                else:
                    return "<b>Beschluss:</b> {}".format(self.values[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
269
270
271
            elif self.name == "footnote":
                return '<sup id="#fnref{0}"><a href="#fn{0}">Fn</a></sup>'.format(
                    footnote_hash(self.values[0]))
Robin Sonnabend's avatar
Robin Sonnabend committed
272
273
        else:
            raise _not_implemented(self, render_type)
Robin Sonnabend's avatar
Robin Sonnabend committed
274
275
276
277

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

    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
281
    def parse(match, current, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
282
283
284
285
286
287
        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
288
        return Tag(parts[0], parts[1:], linenumber, current)
289
290
291
292
    
    # v1: matches [text without semicolons]
    #PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]"
    # v2: needs at least two parts separated by a semicolon
293
294
295
296
    #PATTERN = r"\[(?<content>(?:[^;\]]*;)+(?:[^;\]]*))\]"
    # v3: also match [] without semicolons inbetween, as there is not other use for that
    PATTERN = r"\[(?<content>[^\]]*)\]"

Robin Sonnabend's avatar
Robin Sonnabend committed
297
    KNOWN_TAGS = ["todo", "url", "beschluss", "footnote"]
Robin Sonnabend's avatar
Robin Sonnabend committed
298
299
300


class Empty(Element):
301
302
    def __init__(self, linenumber):
        linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
303

Robin Sonnabend's avatar
Robin Sonnabend committed
304
    def render(self, render_type, show_private, level=None, protocol=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
305
306
307
308
309
        return ""

    def dump(self, level=None):
        if level is None:
            level = 0
310
        return "{}empty".format(INDENT_LETTER * level)
Robin Sonnabend's avatar
Robin Sonnabend committed
311
312
313
314
315
316

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

317
    PATTERN = r"(?:\s+|;)"
Robin Sonnabend's avatar
Robin Sonnabend committed
318
319

class Remark(Element):
320
    def __init__(self, name, value, linenumber):
Robin Sonnabend's avatar
Robin Sonnabend committed
321
322
        self.name = name
        self.value = value
323
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
324

Robin Sonnabend's avatar
Robin Sonnabend committed
325
326
327
328
329
330
331
    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)
332
333
334
335
336
        elif render_type == RenderType.html:
            return "<p>{}: {}</p>".format(self.name, self.value)
        else:
            raise _not_implemented(self, render_type)
            
Robin Sonnabend's avatar
Robin Sonnabend committed
337
338
339
340

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

Robin Sonnabend's avatar
Robin Sonnabend committed
343
344
345
    def get_tags(self, tags):
        return tags

Robin Sonnabend's avatar
Robin Sonnabend committed
346
347
348
349
350
351
352
353
354
355
    @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
356
        element = Remark(name, value, linenumber)
Robin Sonnabend's avatar
Robin Sonnabend committed
357
358
359
360
361
362
        current = Element.parse_outer(element, current)
        return current, linenumber

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

class Fork(Element):
363
364
    def __init__(self, is_top, name, parent, linenumber, children=None):
        self.is_top = is_top
365
        self.name = name.strip() if (name is not None and len(name) > 0) else None
Robin Sonnabend's avatar
Robin Sonnabend committed
366
        self.parent = parent
367
        self.linenumber = linenumber
Robin Sonnabend's avatar
Robin Sonnabend committed
368
369
370
371
372
        self.children = [] if children is None else children

    def dump(self, level=None):
        if level is None:
            level = 0
373
        result_lines = ["{}fork: {}'{}'".format(INDENT_LETTER * level, "TOP " if self.is_top else "", self.name)]
Robin Sonnabend's avatar
Robin Sonnabend committed
374
        for child in self.children:
375
376
            result_lines.append(child.dump(level + 1))
        return "\n".join(result_lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
377

Robin Sonnabend's avatar
Robin Sonnabend committed
378
    def test_private(self, name):
379
380
        if name is None:
            return False
Robin Sonnabend's avatar
Robin Sonnabend committed
381
        stripped_name = name.replace(":", "").strip()
Robin Sonnabend's avatar
Robin Sonnabend committed
382
        return stripped_name in config.PRIVATE_KEYWORDS
Robin Sonnabend's avatar
Robin Sonnabend committed
383

Robin Sonnabend's avatar
Robin Sonnabend committed
384
    def render(self, render_type, show_private, level, protocol=None):
385
        name_line = self.name if self.name is not None else ""
Robin Sonnabend's avatar
Robin Sonnabend committed
386
387
        if level == 0 and self.name == "Todos" and not show_private:
            return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
388
389
390
391
392
393
394
395
396
397
398
399
        if render_type == RenderType.latex:
            begin_line = r"\begin{itemize}"
            end_line = r"\end{itemize}"
            content_parts = []
            for child in self.children:
                part = child.render(render_type, show_private, level=level+1, protocol=protocol)
                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)
400
401
            if len(content_lines.strip()) == 0:
                content_lines = "\\item Nichts\n"
Robin Sonnabend's avatar
Robin Sonnabend committed
402
403
404
405
            if level == 0:
                return "\n".join([begin_line, content_lines, end_line])
            elif self.test_private(self.name):
                if show_private:
406
                    return (r"\begin{tcolorbox}[breakable,title=Interner Abschnitt]" + "\n"
407
408
                            + r"\begin{itemize}" + "\n"
                            + content_lines + "\n"
409
410
                            + r"\end{itemize}" + "\n"
                            + r"\end{tcolorbox}")
Robin Sonnabend's avatar
Robin Sonnabend committed
411
                else:
412
                    return r"\textit{[An dieser Stelle wurde intern protokolliert.]}"
Robin Sonnabend's avatar
Robin Sonnabend committed
413
            else:
414
                return "\n".join([escape_tex(name_line), begin_line, content_lines, end_line])
Robin Sonnabend's avatar
Robin Sonnabend committed
415
        elif render_type == RenderType.wikitext:
Robin Sonnabend's avatar
Robin Sonnabend committed
416
            title_line = "{0} {1} {0}".format("=" * (level + 2), name_line)
Robin Sonnabend's avatar
Robin Sonnabend committed
417
418
419
420
421
422
            content_parts = []
            for child in self.children:
                part = child.render(render_type, show_private, level=level+1, protocol=protocol)
                if len(part.strip()) == 0:
                    continue
                content_parts.append(part)
Robin Sonnabend's avatar
Robin Sonnabend committed
423
            content_lines = "{}\n\n{}\n".format(title_line, "\n\n".join(content_parts))
Robin Sonnabend's avatar
Robin Sonnabend committed
424
            if self.test_private(self.name) and not show_private:
Robin Sonnabend's avatar
Robin Sonnabend committed
425
                return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
426
427
428
429
430
431
            else:
                return content_lines
        elif render_type == RenderType.plaintext:
            title_line = "{} {}".format("#" * (level + 1), name_line)
            content_parts = []
            for child in self.children:
432
                part = child.render(render_type, show_private, level=level+1, protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
433
434
435
436
437
438
439
440
                if len(part.strip()) == 0:
                    continue
                content_parts.append(part)
            content_lines = "{}\n{}".format(title_line, "\n".join(content_parts))
            if self.test_private(self.name) and not show_private:
                return ""
            else:
                return content_lines
441
        elif render_type == RenderType.html:
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
            depth = level + 1 + getattr(config, "HTML_LEVEL_OFFSET", 0)
            content_lines = ""
            if depth < 5:
                title_line = "<h{depth}>{content}</h{depth}>".format(depth=depth, content=name_line)
                content_parts = []
                for child in self.children:
                    part = child.render(render_type, show_private, level=level+1, protocol=protocol)
                    if len(part.strip()) == 0:
                        continue
                    content_parts.append("<p>{}</p>".format(part))
                content_lines = "{}\n\n{}".format(title_line, "\n".join(content_parts))
            else:
                content_parts = []
                for child in self.children:
                    part = child.render(render_type, show_private, level=level+1, protocol=protocol)
                    if len(part.strip()) == 0:
                        continue
                    content_parts.append("<li>{}</li>".format(part))
                content_lines = "{}\n<ul>\n{}\n</ul>".format(name_line, "\n".join(content_parts))
461
462
463
464
            if self.test_private(self.name) and not show_private:
                return ""
            else:
                return content_lines
Robin Sonnabend's avatar
Robin Sonnabend committed
465
        else:
Robin Sonnabend's avatar
Robin Sonnabend committed
466
467
            raise _not_implemented(self, render_type)

Robin Sonnabend's avatar
Robin Sonnabend committed
468

469
470
471
472
473
474
475
    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
476
    def is_anonymous(self):
477
        return self.name == None
Robin Sonnabend's avatar
Robin Sonnabend committed
478
479
480
481

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

Robin Sonnabend's avatar
Robin Sonnabend committed
482
483
484
485
486
    def get_top(self):
        if self.is_root() or self.parent.is_root():
            return self
        return self.parent.get_top()

487
488
489
490
491
492
493
494
495
496
    def get_top_number(self):
        if self.is_root():
            return 1
        top = self.get_top()
        tops = [child
            for child in top.parent.children
            if isinstance(child, Fork)
        ]
        return tops.index(top) + 1

497
498
499
500
501
502
503
504
505
506
507
    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
508
509
510
511
512
513
    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)
514
515
516
                if isinstance(child, Content):
                    elements.update(child.children)
                elif isinstance(child, Fork):
Robin Sonnabend's avatar
Robin Sonnabend committed
517
518
519
                    child.get_visible_elements(show_private, elements)
        return elements

Robin Sonnabend's avatar
Robin Sonnabend committed
520
521
    @staticmethod
    def create_root():
522
        return Fork(None, None, None, 0)
Robin Sonnabend's avatar
Robin Sonnabend committed
523
524
525
526

    @staticmethod
    def parse(match, current, linenumber=None):
        linenumber = Element.parse_inner(match, current, linenumber)
527
528
529
530
531
532
533
        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
534
535
536
537
538
539
540
541
542
543
544
545
546
547
        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():
            raise ParserException("Found end tag for root element!", linenumber)
        current = current.parent
        return current, linenumber

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

548
549
550
    # v1: has a problem with old protocols that do not use a lot of semicolons
    #PATTERN = r"\s*(?<name1>[^{};]+)?{(?<environment>\S+)?\h*(?<name2>[^\n]+)?"
    # v2: do not allow newlines in name1 or semicolons in name2
551
552
553
554
555
    #PATTERN = r"\s*(?<name1>[^{};\n]+)?{(?<environment>[^\s{};]+)?\h*(?<name2>[^;{}\n]+)?"
    # v3: no environment/name2 for normal lists, only for tops
    #PATTERN = r"\s*(?<name>[^{};\n]+)?{(?:TOP\h*(?<topname>[^;{}\n]+))?"
    # v4: do allow one newline between name and {
    PATTERN = r"\s*(?<name>(?:[^{};\n])+)?\n?\s*{(?:TOP\h*(?<topname>[^;{}\n]+))?"
Robin Sonnabend's avatar
Robin Sonnabend committed
556
557
558
559
560
561
562
563
564
565
566
    END_PATTERN = r"\s*};?"

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([
567
568
    (re.compile(Tag.PATTERN), Tag.parse),
    (re.compile(Text.PATTERN), Text.parse)
Robin Sonnabend's avatar
Robin Sonnabend committed
569
570
571
572
573
574
575
576
577
578
579
580
])

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()):]
581
582
583
584
585
                try:
                    current, linenumber = PATTERNS[pattern](match, current, linenumber)
                except ParserException as exc:
                    exc.tree = tree
                    raise exc
Robin Sonnabend's avatar
Robin Sonnabend committed
586
587
588
                found = True
                break
        if not found:
589
            raise ParserException("No matching syntax element found!", linenumber, tree=tree)
Robin Sonnabend's avatar
Robin Sonnabend committed
590
    if current is not tree:
591
        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
592
593
    return tree

Robin Sonnabend's avatar
Robin Sonnabend committed
594
def main(test_file_name=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
595
    source = ""
Robin Sonnabend's avatar
Robin Sonnabend committed
596
597
    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
598
        source = f.read()
Robin Sonnabend's avatar
Robin Sonnabend committed
599
600
    try:
        tree = parse(source)
601
        print(tree.dump())
Robin Sonnabend's avatar
Robin Sonnabend committed
602
603
604
605
    except ParserException as e:
        print(e)
    else:
        print("worked!")
Robin Sonnabend's avatar
Robin Sonnabend committed
606
607
608
    

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