"""Konwersja tekst ↔ symbol astrologiczny (LOG-22). Litery i słowa (Sa Pis 26°08' conj Fortune) ↔ glify (♄ ♓ 26°08' ☌ ⊗). Potrzebne pod rysowanie kosmogramu (PRE-12) — planety i znaki na kole rysujemy symbolami. DWIE zasady z bazy wymagań, obie krytyczne: * DAN-18: TYLKO tekstowy Unicode, NIGDY emoji. Część znaków (zodiak, ♀, ♂) ma domyślnie prezentację emoji — kolorowy kwadrat zamiast czarno-białego glifu, nieczytelny i niesterowalny przez CSS. Wymuszamy prezentację tekstową selektorem wariantu U+FE0E (NIE U+FE0F, który robi odwrotnie). * DAN-17: łańcuch musi znosić dowolny Unicode. Reverse (symbol→tekst) normalizuje wejście, zdejmując selektory wariantu, żeby glif z FE0E i bez dawał ten sam wynik. Glify zapisane przez \\u — jednoznaczne code-pointy, odporne na zniekształcenia edytorów i samodokumentujące (widać, który to znak Unicode). """ from __future__ import annotations # selektory wariantu prezentacji _TEXT = "︎" # VS15 — wymusza glif tekstowy (czarno-biały) _EMOJI = "️" # VS16 — prezentacja emoji; NIGDY nie emitujemy, zdejmujemy przy reverse def _t(cp: str) -> str: """Znak z wymuszoną prezentacją tekstową (dokleja VS15).""" return cp + _TEXT # ── planety i światła ──────────────────────────────────────────────────── # ♀ i ♂ mają wariant emoji → wymuszamy tekst; reszta jest tekstowa domyślnie. PLANET = { "Sun": "☉", # ☉ "Moon": "☽", # ☽ "Mercury": "☿", # ☿ "Venus": _t("♀"), # ♀︎ "Mars": _t("♂"), # ♂︎ "Jupiter": "♃", # ♃ "Saturn": "♄", # ♄ "Uranus": "♅", # ♅ "Neptune": "♆", # ♆ "Pluto": "♇", # ♇ } # ── punkty wirtualne ───────────────────────────────────────────────────── POINT = { "North Node": "☊", # ☊ "South Node": "☋", # ☋ "Lilith": "⚸", # ⚸ (Black Moon Lilith) "Chiron": "⚷", # ⚷ } # ── Lots (punkty arabskie) ─────────────────────────────────────────────── # Standardowy glif ma tylko Fortuna (⊗). Reszta Lotów nie ma powszechnie # wspieranego symbolu → zwracamy None i UI pokazuje nazwę. LOT = { "Fortune": "⊗", # ⊗ "Part of Fortune": "⊗", } # ── znaki zodiaku (wszystkie mają wariant emoji → wszystkie z VS15) ────── _SIGN_CP = { "Aries": "♈", "Taurus": "♉", "Gemini": "♊", "Cancer": "♋", "Leo": "♌", "Virgo": "♍", "Libra": "♎", "Scorpio": "♏", "Sagittarius": "♐", "Capricorn": "♑", "Aquarius": "♒", "Pisces": "♓", } SIGN = {name: _t(cp) for name, cp in _SIGN_CP.items()} # ── aspekty (nazwy jak w aspects.py + minor z abbreviations) ───────────── ASPECT = { "conjunction": "☌", # ☌ "opposition": "☍", # ☍ "trine": "△", # △ "square": "□", # □ "sextile": "⚹", # ⚹ "semisextile": "⚺", # ⚺ "quincunx": "⚻", # ⚻ "semisquare": "∠", # ∠ } # ── ruch ───────────────────────────────────────────────────────────────── RETROGRADE = "℞" # ℞ DIRECT = "D" # zwykłe „D" — brak dedykowanego glifu prostego ruchu # ── forward: nazwa → glif ──────────────────────────────────────────────── def glyph_for(name: str) -> str | None: """Glif obiektu/punktu/Lota po nazwie (jak w silniku). None, gdy brak.""" return PLANET.get(name) or POINT.get(name) or LOT.get(name) def sign_glyph(sign: str) -> str | None: return SIGN.get(sign) def aspect_glyph(aspect: str) -> str | None: return ASPECT.get(aspect) # ── reverse: glif → nazwa ──────────────────────────────────────────────── def _strip_variants(g: str) -> str: """Zdejmuje selektory wariantu — glif z FE0E i bez daje ten sam klucz.""" return g.replace(_TEXT, "").replace(_EMOJI, "") def _reverse(mapping: dict[str, str]) -> dict[str, str]: # pierwsze wystąpienie wygrywa (Fortune vs Part of Fortune → 'Fortune') out: dict[str, str] = {} for name, g in mapping.items(): out.setdefault(_strip_variants(g), name) return out _PLANET_POINT_REV = _reverse({**PLANET, **POINT, **{"Fortune": LOT["Fortune"]}}) _SIGN_REV = _reverse(SIGN) _ASPECT_REV = _reverse(ASPECT) def name_for_glyph(g: str) -> str | None: """Obiekt/punkt po glifie (znosi obecność lub brak selektora wariantu).""" return _PLANET_POINT_REV.get(_strip_variants(g)) def sign_for_glyph(g: str) -> str | None: return _SIGN_REV.get(_strip_variants(g)) def aspect_for_glyph(g: str) -> str | None: return _ASPECT_REV.get(_strip_variants(g)) # ── glifikacja tekstu sygnifikatora (składnia [XX z bazy) ──────────────── # Nasze sygnifikatory zapisane są tokenami z prefiksem [ (np. „[Sa [conj [PF"). # glyphify zamienia znane tokeny na glify, resztę zostawia. To druga strona # abbreviations.expand: tam token → słowo, tu token → symbol. import re as _re # token po nawiasie (np. „Su", „Tau", „conj", „PF") → glif _TOKEN_GLYPH: dict[str, str] = {} _ABBR_TO_NAME = { "Su": "Sun", "Mo": "Moon", "Me": "Mercury", "Ve": "Venus", "Ma": "Mars", "Ju": "Jupiter", "Sa": "Saturn", "Ur": "Uranus", "Ne": "Neptune", "Pl": "Pluto", "NN": "North Node", "SN": "South Node", "Lilith": "Lilith", "Chiron": "Chiron", "PF": "Fortune", "Fortune": "Fortune", } for _abbr, _name in _ABBR_TO_NAME.items(): _g = glyph_for(_name) if _g: _TOKEN_GLYPH[_abbr] = _g _SIGN_ABBR_NAME = { "Ari": "Aries", "Tau": "Taurus", "Gem": "Gemini", "Can": "Cancer", "Leo": "Leo", "Vir": "Virgo", "Lib": "Libra", "Sco": "Scorpio", "Sag": "Sagittarius", "Cap": "Capricorn", "Aqu": "Aquarius", "Pis": "Pisces", } for _abbr, _name in _SIGN_ABBR_NAME.items(): _TOKEN_GLYPH[_abbr] = SIGN[_name] _ASP_ABBR_NAME = { "conj": "conjunction", "opp": "opposition", "tri": "trine", "sq": "square", "sex": "sextile", "semisex": "semisextile", "quincunx": "quincunx", "semisq": "semisquare", } for _abbr, _name in _ASP_ABBR_NAME.items(): if _name in ASPECT: _TOKEN_GLYPH[_abbr] = ASPECT[_name] _TOKEN_RE = _re.compile(r"\[([A-Za-z]+)") def glyphify(text: str) -> str: """Zamienia tokeny [XX na glify; nieznane zostawia bez zmiany. Dodatkowo »Rx«/»R« → ℞. Nie parsuje stopni — te i tak są czytelne (26°08').""" if not text: return text out = _TOKEN_RE.sub(lambda m: _TOKEN_GLYPH.get(m.group(1), m.group(0)), text) out = _re.sub(r"\bR[x]?\b", RETROGRADE, out) return out