summaryrefslogtreecommitdiffstats
path: root/src/parser.py
blob: 69718e57994972a4842ce195f0c10348006f8c51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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
87
88
89
90
91
92
93
94
95
96
from pathlib import Path
from collections import namedtuple
from enum import Enum
from typing import TextIO, Iterator

Card = namedtuple('Card', ['front', 'back'])

def _getCard(front_lines: list[str], back_lines: list[str]) -> tuple[int, Card]:
    front_text = "".join(front_lines).strip()
    back_text = "".join(back_lines).strip()
    card = Card(front_text, back_text)
    id = hash(card)
    return id, card

def _getCards(f: TextIO) -> Iterator[tuple[id, Card]]:
    class State(Enum):
        PARSE_FRONT = 1,
        PARSE_BACK = 2
    
    state = None
    front_lines = []
    back_lines = []

    for i, line in enumerate(f):
        match line.strip():
            case "FRONT":
                # Edge case: FRONT twice in a row
                if state == State.PARSE_FRONT:
                    raise Exception(f"Unexpected 'FRONT': {f}:{i}")

                # Next card is starting, wrap up current one
                if state == State.PARSE_BACK:
                    yield _getCard(front_lines, back_lines)
                    front_lines.clear()
                    back_lines.clear()

                state = State.PARSE_FRONT

            case "BACK":
                # Edge case: BACK without FRONT before it
                if state != State.PARSE_FRONT:
                    raise Exception(f"Unexpected 'BACK': {f}:{i}")

                state = State.PARSE_BACK
                
            case _:
                match state:
                    case State.PARSE_FRONT:
                        front_lines += line
                    case State.PARSE_BACK:
                        back_lines += line
                    # Edge case: file does not start with FRONT, flush preamble
                    case _:
                        continue

    # Edge case: file did not end with contents of BACK
    if state == State.PARSE_FRONT:
        raise Exception(f"Unexpected end of file")

    # Edge case: file was empty
    if state is None:
        return

    yield _getCard(front_lines, back_lines)

def parse(path: Path) -> dict[int, Card]:
    """
    Parse a .fcard file and return a list of Card instances.

    The parser expects .fcard files in the following format:

    FRONT
    This is the front of the first card.

    BACK
    This is the back of the first card.

    FRONT
    This is another card.

    Multiple lines on the front are allowed.

    BACK
    Multiple lines on the back?

    Also allowed.

    FRONT
    ...
    """
    if not path.is_file():
        print(f"[Warning] Not a file: {path}")
        return {}

    with open(path, "r") as f:
        return {id : card for id, card in _getCards(f)}