Partial state.
parent
54411fbf70
commit
0b629044a8
|
@ -0,0 +1,36 @@
|
||||||
|
# anki-synonyms
|
||||||
|
|
||||||
|
A simple [Anki](https://apps.ankiweb.net/) plugin that allows randomly choosing
|
||||||
|
different options for parts of prompts. This was designed to handle synonyms in
|
||||||
|
a clean way.
|
||||||
|
|
||||||
|
Consider a [total order](https://en.wikipedia.org/wiki/Total_order). What this
|
||||||
|
is does not matter; what it could also be called does. What some people call a
|
||||||
|
"total order", others call a "linear order". Though this example is simple, it
|
||||||
|
does highlight an issue - remembering the various synonyms used to describe
|
||||||
|
a concept is important for fluency.
|
||||||
|
|
||||||
|
As of now, to handle this situation, it is probably best to use two flashcards,
|
||||||
|
one with prompt "Total Order" and another with prompt "Linear Order". In some
|
||||||
|
cases though, it'd be nice if the flashcard could *choose* which term it shows
|
||||||
|
when it shows it. That is, it'd be nice to have a single card and allow Anki to
|
||||||
|
randomly choose to show "Total Order" *or* "Linear Order".
|
||||||
|
|
||||||
|
To do so, we can install this plugin and write the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
'(Total|Linear) Order
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `'(` is used to indicate the start of a set of choices Anki can display,
|
||||||
|
`|` is used to separate the different options, and `)` is used to indicate the
|
||||||
|
end of the set. The result is either "Total Order" or "Linear Order" at time
|
||||||
|
of prompting.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Nesting
|
||||||
|
|
||||||
|
TODO
|
|
@ -0,0 +1,121 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import enum
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
START_TAG = "'("
|
||||||
|
END_TAG = ")"
|
||||||
|
CHOICE_TAG = "|"
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(enum.Enum):
|
||||||
|
START = 0
|
||||||
|
END = 1
|
||||||
|
CHOICE = 2
|
||||||
|
|
||||||
|
|
||||||
|
Token = Union[Tag, str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParserState:
|
||||||
|
"""Convenience class used when traversing our tokenized stream."""
|
||||||
|
|
||||||
|
starts: int
|
||||||
|
pos: int
|
||||||
|
tokens: list[Token]
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_at(arg: str, target: str, pos: int = 0) -> bool:
|
||||||
|
"""Check a substring matches the @target parameter."""
|
||||||
|
return arg[pos : pos + len(target)] == target
|
||||||
|
|
||||||
|
|
||||||
|
def _label_tokens(arg: str) -> ParserState:
|
||||||
|
"""Primary lexing function which traverses our stream and assigns initial
|
||||||
|
token labels.
|
||||||
|
|
||||||
|
Note this is a greedy algorithm so it is possible we incorrectly label
|
||||||
|
tokens as 'START'. For instance, consider a start tag of "'(". Then running
|
||||||
|
|
||||||
|
>>> _label_tokens(arg="hello'(")
|
||||||
|
|
||||||
|
will yield a token stream like ["hello", START] when we should have just a
|
||||||
|
single entry "hello'(". This gets corrected in `_relabel_starts`.
|
||||||
|
"""
|
||||||
|
state = ParserState(starts=0, pos=0, tokens=[])
|
||||||
|
while state.pos < len(arg):
|
||||||
|
if _matches_at(arg, target=START_TAG, pos=state.pos):
|
||||||
|
state.tokens.append(Tag.START)
|
||||||
|
state.starts += 1
|
||||||
|
state.pos += len(START_TAG)
|
||||||
|
elif state.starts and _matches_at(arg, target=END_TAG, pos=state.pos):
|
||||||
|
state.tokens.append(Tag.END)
|
||||||
|
state.starts -= 1
|
||||||
|
state.pos += len(END_TAG)
|
||||||
|
elif state.starts and _matches_at(arg, target=CHOICE_TAG, pos=state.pos):
|
||||||
|
state.tokens.append(Tag.CHOICE)
|
||||||
|
state.pos += 1
|
||||||
|
else:
|
||||||
|
state.tokens.append(arg[state.pos])
|
||||||
|
state.pos += 1
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def _relabel_starts(arg: str, state: ParserState) -> ParserState:
|
||||||
|
"""Relabels 'START' tags that may have been labeled incorrectly."""
|
||||||
|
new_state = copy.copy(state)
|
||||||
|
if not new_state.starts:
|
||||||
|
return new_state
|
||||||
|
for i, token in enumerate(reversed(new_state.tokens)):
|
||||||
|
if token != Tag.START:
|
||||||
|
continue
|
||||||
|
index = len(new_state.tokens) - i - 1
|
||||||
|
new_state.tokens[index] = START_TAG
|
||||||
|
new_state.starts -= 1
|
||||||
|
if not new_state.starts:
|
||||||
|
break
|
||||||
|
return new_state
|
||||||
|
|
||||||
|
|
||||||
|
def _group_tokens(state: ParserState) -> list[Token]:
|
||||||
|
"""Aggregate adjacent strings together into a single token."""
|
||||||
|
new_tokens: list[Token] = []
|
||||||
|
for token in state.tokens:
|
||||||
|
if new_tokens and isinstance(token, str) and isinstance(new_tokens[-1], str):
|
||||||
|
new_tokens[-1] += token
|
||||||
|
else:
|
||||||
|
new_tokens.append(token)
|
||||||
|
return new_tokens
|
||||||
|
|
||||||
|
|
||||||
|
def _tokenize(arg: str) -> list[Token]:
|
||||||
|
"""Break string into token stream for easier handling."""
|
||||||
|
state = _label_tokens(arg)
|
||||||
|
state = _relabel_starts(arg, state)
|
||||||
|
return _group_tokens(state)
|
||||||
|
|
||||||
|
|
||||||
|
def run_parser(arg: str) -> str:
|
||||||
|
"""Find all "choice" selections within the given @arg.
|
||||||
|
|
||||||
|
For instance, assuming a START, END, and CHOICE of "'(", ")", and "|"
|
||||||
|
respectively, parsing "'(hello|world)" yields either "hello" or "world".
|
||||||
|
"""
|
||||||
|
tokens = _tokenize(arg)
|
||||||
|
stack: list[list[str]] = [[]]
|
||||||
|
for token in tokens:
|
||||||
|
if token is Tag.START:
|
||||||
|
stack.append([])
|
||||||
|
elif token is Tag.END:
|
||||||
|
ts = stack.pop()
|
||||||
|
stack[-1].append(random.choice(ts))
|
||||||
|
elif token is Tag.CHOICE:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
stack[-1].append(token)
|
||||||
|
assert len(stack) == 1, "Stack is larger than a single element"
|
||||||
|
return "".join(stack[0])
|
Loading…
Reference in New Issue