diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-24 06:47:07 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-24 06:47:07 -0400 |
| commit | 060a938b9629b134b9110db4d1cbc99577ee5674 (patch) | |
| tree | 0e240a6b44fce13fc8d33f019cef0fe8e01e4bbb /.ai/scripts/flashcard-to-anki.py | |
| parent | 0d87c80b3377e47b1ba56dc38e1f7162fd0ef9d7 (diff) | |
| download | rulesets-060a938b9629b134b9110db4d1cbc99577ee5674.tar.gz rulesets-060a938b9629b134b9110db4d1cbc99577ee5674.zip | |
fix(flashcard): name the Anki deck from #+TITLE, not the filename
flashcard-to-anki.py's default_deck_name returned the input basename, so a deck built through flashcard-sync (which passes no --deck) was named after the file slug (refutation-drill) instead of the curated #+TITLE the phone deck should read (Refutations). flashcard-review.org already documented the #+TITLE behavior, and the script never matched it. default_deck_name now scans for a #+TITLE line (case-insensitive, trimmed) and falls back to the basename when there's none.
Five new tests cover title-drives-name, trimming, case-insensitivity, and the two basename fallbacks. The two old tests that asserted basename-always are replaced. The pre-staged script and test (validated 2026-06-21) applied cleanly red-to-green; their redundant copies are removed and the rationale doc kept.
Migration: deck IDs derive from the name, so decks previously built without --deck land as new decks on the next import. Old basename-named decks keep their history and can be deleted by hand.
Claude-Session: https://claude.ai/code/session_017PtX1nt1rtYVATuzmzBS4f
Diffstat (limited to '.ai/scripts/flashcard-to-anki.py')
| -rwxr-xr-x | .ai/scripts/flashcard-to-anki.py | 26 |
1 files changed, 20 insertions, 6 deletions
diff --git a/.ai/scripts/flashcard-to-anki.py b/.ai/scripts/flashcard-to-anki.py index 7227683..ca4c70b 100755 --- a/.ai/scripts/flashcard-to-anki.py +++ b/.ai/scripts/flashcard-to-anki.py @@ -13,9 +13,11 @@ Parses org-drill structure: text (sans :drill: tag). Back = entry body with newlines converted to <br>. -Deck name defaults to the input basename, case preserved. Deck and model -IDs are derived from the deck name via stable hash so re-importing the -same deck updates existing cards instead of duplicating them. +Deck name defaults to the org #+TITLE: (so the phone deck reads as the +curated title), falling back to the input basename when the source has +no #+TITLE. Deck and model IDs are derived from the deck name via stable +hash so re-importing the same deck updates existing cards instead of +duplicating them. Output defaults to ~/sync/phone/anki/<input-basename>.apkg. The .apkg is a mobile-Anki artifact the phone picks up from its sync dir, so it lands @@ -177,7 +179,19 @@ def build(cards: list[tuple[str, str, str]], deck_name: str) -> genanki.Deck: return deck -def default_deck_name(input_path: Path) -> str: +def default_deck_name(input_path: Path, org_text: str) -> str: + """Deck name defaults to the org #+TITLE:, falling back to the basename. + + The #+TITLE drives both the org-drill display in Emacs and the Anki + deck name on the phone, so the consumed deck reads as the curated + title ("Refutations") rather than the filename slug + ("refutation-drill"). Falls back to the input basename (case + preserved) when the source has no non-empty #+TITLE line. + """ + for line in org_text.splitlines(): + m = re.match(r"^#\+TITLE:\s*(.*\S)\s*$", line, re.IGNORECASE) + if m: + return m.group(1).strip() return input_path.stem @@ -197,7 +211,7 @@ def main() -> int: ) parser.add_argument( "--deck", - help="Deck name. Defaults to the input basename.", + help="Deck name. Defaults to the org #+TITLE, or the input basename.", ) parser.add_argument( "--output", @@ -213,7 +227,7 @@ def main() -> int: return 1 org_text = input_path.read_text(encoding="utf-8") - deck_name = args.deck or default_deck_name(input_path) + deck_name = args.deck or default_deck_name(input_path, org_text) output_path: Path = (args.output or default_output_path(input_path)).expanduser().resolve() output_path.parent.mkdir(parents=True, exist_ok=True) |
