"""
maps
~~~~
A module for handling :ref:`YADN` dice maps.
"""
from typing import Callable, Optional
from yadr.base import BaseLexer, _mutable
from yadr.model import NamedMap, Result, Token, TokenInfo, symbols
# Lexing.
[docs]
class Lexer(BaseLexer):
"""A state machine to lex dice maps in :ref:`YADN` dice notation."""
def __init__(self) -> None:
state_map: dict[Token, Callable] = {
Token.START: self._start,
Token.END: self._start,
Token.KV_DELIMITER: self._kv_delimiter,
Token.MAP_CLOSE: self._map_close,
Token.MAP_OPEN: self._map_open,
Token.NAME_DELIMITER: self._name_delimiter,
Token.NEGATIVE_SIGN: self._negative_sign,
Token.NUMBER: self._number,
Token.PAIR_DELIMITER: self._pair_delimiter,
Token.QUALIFIER: self._qualifier,
Token.QUALIFIER_END: self._qualifier_end,
Token.WHITESPACE: self._whitespace,
}
symbol_map: dict[Token, list[str]] = symbols
bracket_states: dict[Token, Token] = {
Token.NEGATIVE_SIGN: Token.NUMBER,
Token.QUALIFIER_DELIMITER: Token.QUALIFIER,
}
bracket_ends: dict[Token, Token] = {
Token.QUALIFIER: Token.QUALIFIER_END,
}
result_map: dict[Token, Callable] = {
Token.NUMBER: self._tf_number,
Token.QUALIFIER: self._tf_qualifier,
}
no_store: list[Token] = [
Token.START,
Token.QUALIFIER_END,
Token.WHITESPACE,
]
init_state: Token = Token.START
super().__init__(
state_map,
symbol_map,
bracket_states,
bracket_ends,
result_map,
no_store,
init_state
)
# Result transformation rules.
def _tf_number(self, value: str) -> int:
return int(value)
def _tf_qualifier(self, value: str) -> str:
return value[1:-1]
# Lexing rules.
def _kv_delimiter(self, char: str) -> None:
"""Lex a key-value delimiter symbol."""
can_follow = [
Token.NEGATIVE_SIGN,
Token.NUMBER,
Token.QUALIFIER_DELIMITER,
Token.WHITESPACE,
]
self._check_char(char, can_follow)
def _map_close(self, char: str) -> None:
"""Lex a map close symbol."""
can_follow: list[Token] = []
self._check_char(char, can_follow)
def _map_open(self, char: str) -> None:
"""Lex a map open symbol."""
can_follow = [
Token.MAP_CLOSE,
Token.QUALIFIER_DELIMITER,
Token.WHITESPACE,
]
self._check_char(char, can_follow)
def _name_delimiter(self, char: str) -> None:
"""Lex a name delimiter symbol."""
can_follow = [
Token.NEGATIVE_SIGN,
Token.NUMBER,
Token.WHITESPACE,
]
self._check_char(char, can_follow)
def _number(self, char: str) -> None:
"""Processing a number."""
can_follow = [
Token.KV_DELIMITER,
Token.MAP_CLOSE,
Token.PAIR_DELIMITER,
Token.WHITESPACE,
]
# Check here if the character is a digit because the checks in
# Char are currently limited to tokens that no longer than two
# characters. Check if the state is a number because white
# space also ends up here, and we want white space to separate
# numbers.
if char.isdigit() and self.state == Token.NUMBER:
self.buffer += char
else:
self._check_char(char, can_follow)
def _negative_sign(self, char: str) -> None:
"""Processing a number."""
can_follow = [
Token.NUMBER,
]
self._check_char(char, can_follow)
def _pair_delimiter(self, char: str) -> None:
"""Lex a pair delimiter symbol."""
can_follow = [
Token.NEGATIVE_SIGN,
Token.NUMBER,
Token.WHITESPACE,
]
self._check_char(char, can_follow)
def _qualifier(self, char: str) -> None:
"""Lex a qualifier."""
self.buffer += char
if self._is_token_start(Token.QUALIFIER_DELIMITER, char):
new_state = Token.QUALIFIER_END
self._change_state(new_state, char)
def _qualifier_end(self, char: str) -> None:
can_follow = [
Token.MAP_CLOSE,
Token.NAME_DELIMITER,
Token.PAIR_DELIMITER,
Token.WHITESPACE,
]
self._check_char(char, can_follow)
def _start(self, char: str) -> None:
"""Initial lexer state."""
if self.tokens:
self.tokens = []
can_follow = [
Token.MAP_OPEN,
Token.WHITESPACE,
]
self._check_char(char, can_follow)
# Parsing.
[docs]
class Parser:
"""A state machine for parsing :ref:`YADN` dice maps."""
def __init__(self) -> None:
self.name: str = ''
self.pairs: list[tuple[int, str | int]] = []
self.buffer: Optional[int] = None
self.state = Token.START
self.state_map = {
Token.START: self._start,
Token.END: self._start,
Token.KEY: self._key,
Token.NAME: self._name,
Token.VALUE: self._value,
}
[docs]
def parse(self, tokens: tuple[TokenInfo, ...]) -> NamedMap:
"""Parse YADN dice mapping tokens.
:param tokens: A dice map as a sequence of :ref:`YADN` tokens
to parse.
:return: A class defined in :class:`yadr.model.NamedMap`.
:rtype: tuple
"""
for token_info in tokens:
process = self.state_map[self.state]
process(token_info)
return (self.name, {k: v for k, v in self.pairs})
# Parsing rules.
def _key(self, token_info: tuple[Token, Result]) -> None:
token, value = token_info
if token == Token.NUMBER and isinstance(value, int):
self.buffer = value
elif token == Token.KV_DELIMITER:
self.state = Token.VALUE
elif token == Token.MAP_CLOSE:
...
else:
msg = f'KEY cannot contain {token.name}.'
raise ValueError(msg)
def _name(self, token_info: tuple[Token, Result]) -> None:
token, value = token_info
if token == Token.QUALIFIER and isinstance(value, str):
self.name = value
elif token == Token.NAME_DELIMITER:
self.state = Token.KEY
elif token == Token.MAP_CLOSE:
...
else:
msg = f'NAME cannot contain {token.name}.'
raise ValueError(msg)
def _value(self, token_info: tuple[Token, Result]) -> None:
token, value = token_info
if (
isinstance(self.buffer, int)
and (
token == Token.QUALIFIER and isinstance(value, str)
or token == Token.NUMBER and isinstance(value, int)
)
):
key = self.buffer
pair = (key, value)
self.pairs.append(pair)
self.buffer = None
elif token == Token.PAIR_DELIMITER:
self.state = Token.KEY
elif token == Token.MAP_CLOSE:
...
else:
msg = f'VALUE cannot contain {token.name}.'
raise ValueError(msg)
def _start(self, token_info: tuple[Token, Result]) -> None:
token, value = token_info
if token == Token.MAP_OPEN:
self.state = Token.NAME
else:
msg = f'Dice mapping cannot start with a {value}'
raise ValueError(msg)