summaryrefslogtreecommitdiffstats
path: root/flashcards-project/src/flashcards/parser.py
blob: 38abdcc8787a165cca69e3660fd4b8006bf53c6b (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
105
106
107
108
"""
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: card.Card
"""
from pathlib import Path
from enum import Enum
from typing import TextIO, Iterator

from .card import Card, getId

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

def _getCards(f: TextIO) -> Iterator[tuple[str, 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 parseFile(path: str) -> dict[str, Card]:
    """
    Parse a .fcard file and return a dictionary of Card instances indexed by ID.
    """
    with open(path, "r") as f:
        return {id : card for id, card in _getCards(f)}

def parseFiles(paths: list[str]) -> dict[str, Card]:
    """
    Parse a list of .fcard files and return a dictionary of Card instances indexed by ID.
    """
    cards = {}
    for p in paths:
        cards |= parseFile(p)
    return cards