summaryrefslogtreecommitdiffstats
path: root/src/parser.py
blob: 4836bdfecf88e7d09e6d6323da2d16f3f6110966 (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
97
98
99
100
101
102
103
104
"""
Load .fcard files into dictionaries.

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
...

The cards are represented in dictionary entries of the form:

id: (front_text, back_text)
"""

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 dictionary of Card instances indexed by ID.
    """
    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)}