aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.ai/scripts/drill-to-anki.py38
-rw-r--r--.ai/scripts/tests/test_drill_to_anki.py44
-rwxr-xr-xclaude-templates/.ai/scripts/drill-to-anki.py38
-rw-r--r--claude-templates/.ai/scripts/tests/test_drill_to_anki.py44
4 files changed, 114 insertions, 50 deletions
diff --git a/.ai/scripts/drill-to-anki.py b/.ai/scripts/drill-to-anki.py
index 543ccd8..1050021 100755
--- a/.ai/scripts/drill-to-anki.py
+++ b/.ai/scripts/drill-to-anki.py
@@ -13,15 +13,13 @@ Parses org-drill structure:
text (sans :drill: tag). Back = entry body with newlines converted
to <br>.
-Deck name defaults to the org file's #+TITLE if present, otherwise the
-input basename. 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 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.
-Output defaults to ~/sync/org/drill/<input-basename>.apkg (matches
-Craig's project convention where org source lives in the project and
-symlinks into ~/sync/org/drill/, with the .apkg writing directly into
-~/sync/org/drill/ as a build artifact).
+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
+there rather than next to the org source.
Usage:
drill-to-anki.py <input.org>
@@ -91,15 +89,6 @@ def escape_html(s: str) -> str:
)
-def title_from_org(org_text: str) -> str | None:
- """Return the #+TITLE: value if the org file declares one."""
- for line in org_text.splitlines()[:20]:
- m = re.match(r"^#\+TITLE:\s*(.+?)\s*$", line, re.IGNORECASE)
- if m:
- return m.group(1).strip()
- return None
-
-
def strip_org_metadata(body_lines: list[str]) -> list[str]:
"""Drop :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines.
@@ -185,13 +174,13 @@ def build(cards: list[tuple[str, str, str]], deck_name: str) -> genanki.Deck:
return deck
-def default_deck_name(input_path: Path, org_text: str) -> str:
- return title_from_org(org_text) or input_path.stem.replace("-", " ").title()
+def default_deck_name(input_path: Path) -> str:
+ return input_path.stem
def default_output_path(input_path: Path) -> Path:
- drill_dir = Path.home() / "sync" / "org" / "drill"
- return drill_dir / f"{input_path.stem}.apkg"
+ anki_dir = Path.home() / "sync" / "phone" / "anki"
+ return anki_dir / f"{input_path.stem}.apkg"
def main() -> int:
@@ -205,14 +194,13 @@ def main() -> int:
)
parser.add_argument(
"--deck",
- help="Deck name. Defaults to the org file's #+TITLE, or the "
- "input basename if no title is set.",
+ help="Deck name. Defaults to the input basename.",
)
parser.add_argument(
"--output",
type=Path,
help="Output .apkg path. Defaults to "
- "~/sync/org/drill/<input-basename>.apkg.",
+ "~/sync/phone/anki/<input-basename>.apkg.",
)
args = parser.parse_args()
@@ -222,7 +210,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, org_text)
+ deck_name = args.deck or default_deck_name(input_path)
output_path: Path = (args.output or default_output_path(input_path)).expanduser().resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
diff --git a/.ai/scripts/tests/test_drill_to_anki.py b/.ai/scripts/tests/test_drill_to_anki.py
new file mode 100644
index 0000000..6490e58
--- /dev/null
+++ b/.ai/scripts/tests/test_drill_to_anki.py
@@ -0,0 +1,44 @@
+"""Tests for drill-to-anki.py default-path and deck-name helpers.
+
+The script is a PEP 723 uv-run script that imports genanki, which uv resolves
+at runtime but isn't installed in the test environment. The fixture stubs
+genanki in sys.modules so the module loads; the pure helpers under test never
+call into it.
+"""
+from __future__ import annotations
+
+import importlib.util
+import sys
+import types
+from pathlib import Path
+
+import pytest
+
+SCRIPT = Path(__file__).resolve().parents[1] / "drill-to-anki.py"
+
+
+@pytest.fixture(scope="module")
+def drill():
+ # Only stub when genanki is genuinely absent, so a real install isn't shadowed.
+ sys.modules.setdefault("genanki", types.ModuleType("genanki"))
+ spec = importlib.util.spec_from_file_location("drill_to_anki", SCRIPT)
+ assert spec and spec.loader
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+def test_default_output_path_targets_phone_anki_dir(drill):
+ """The .apkg is a phone artifact, so it defaults under sync/phone/anki/."""
+ result = drill.default_output_path(Path("/home/x/projects/health/health-drill.org"))
+ assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg"
+
+
+def test_default_deck_name_is_raw_basename(drill):
+ """Deck name is the input basename with case preserved; #+TITLE is ignored."""
+ assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat"
+
+
+def test_default_deck_name_keeps_hyphens(drill):
+ """A hyphenated basename is kept verbatim rather than title-cased."""
+ assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill"
diff --git a/claude-templates/.ai/scripts/drill-to-anki.py b/claude-templates/.ai/scripts/drill-to-anki.py
index 543ccd8..1050021 100755
--- a/claude-templates/.ai/scripts/drill-to-anki.py
+++ b/claude-templates/.ai/scripts/drill-to-anki.py
@@ -13,15 +13,13 @@ Parses org-drill structure:
text (sans :drill: tag). Back = entry body with newlines converted
to <br>.
-Deck name defaults to the org file's #+TITLE if present, otherwise the
-input basename. 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 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.
-Output defaults to ~/sync/org/drill/<input-basename>.apkg (matches
-Craig's project convention where org source lives in the project and
-symlinks into ~/sync/org/drill/, with the .apkg writing directly into
-~/sync/org/drill/ as a build artifact).
+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
+there rather than next to the org source.
Usage:
drill-to-anki.py <input.org>
@@ -91,15 +89,6 @@ def escape_html(s: str) -> str:
)
-def title_from_org(org_text: str) -> str | None:
- """Return the #+TITLE: value if the org file declares one."""
- for line in org_text.splitlines()[:20]:
- m = re.match(r"^#\+TITLE:\s*(.+?)\s*$", line, re.IGNORECASE)
- if m:
- return m.group(1).strip()
- return None
-
-
def strip_org_metadata(body_lines: list[str]) -> list[str]:
"""Drop :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines.
@@ -185,13 +174,13 @@ def build(cards: list[tuple[str, str, str]], deck_name: str) -> genanki.Deck:
return deck
-def default_deck_name(input_path: Path, org_text: str) -> str:
- return title_from_org(org_text) or input_path.stem.replace("-", " ").title()
+def default_deck_name(input_path: Path) -> str:
+ return input_path.stem
def default_output_path(input_path: Path) -> Path:
- drill_dir = Path.home() / "sync" / "org" / "drill"
- return drill_dir / f"{input_path.stem}.apkg"
+ anki_dir = Path.home() / "sync" / "phone" / "anki"
+ return anki_dir / f"{input_path.stem}.apkg"
def main() -> int:
@@ -205,14 +194,13 @@ def main() -> int:
)
parser.add_argument(
"--deck",
- help="Deck name. Defaults to the org file's #+TITLE, or the "
- "input basename if no title is set.",
+ help="Deck name. Defaults to the input basename.",
)
parser.add_argument(
"--output",
type=Path,
help="Output .apkg path. Defaults to "
- "~/sync/org/drill/<input-basename>.apkg.",
+ "~/sync/phone/anki/<input-basename>.apkg.",
)
args = parser.parse_args()
@@ -222,7 +210,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, org_text)
+ deck_name = args.deck or default_deck_name(input_path)
output_path: Path = (args.output or default_output_path(input_path)).expanduser().resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
diff --git a/claude-templates/.ai/scripts/tests/test_drill_to_anki.py b/claude-templates/.ai/scripts/tests/test_drill_to_anki.py
new file mode 100644
index 0000000..6490e58
--- /dev/null
+++ b/claude-templates/.ai/scripts/tests/test_drill_to_anki.py
@@ -0,0 +1,44 @@
+"""Tests for drill-to-anki.py default-path and deck-name helpers.
+
+The script is a PEP 723 uv-run script that imports genanki, which uv resolves
+at runtime but isn't installed in the test environment. The fixture stubs
+genanki in sys.modules so the module loads; the pure helpers under test never
+call into it.
+"""
+from __future__ import annotations
+
+import importlib.util
+import sys
+import types
+from pathlib import Path
+
+import pytest
+
+SCRIPT = Path(__file__).resolve().parents[1] / "drill-to-anki.py"
+
+
+@pytest.fixture(scope="module")
+def drill():
+ # Only stub when genanki is genuinely absent, so a real install isn't shadowed.
+ sys.modules.setdefault("genanki", types.ModuleType("genanki"))
+ spec = importlib.util.spec_from_file_location("drill_to_anki", SCRIPT)
+ assert spec and spec.loader
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+def test_default_output_path_targets_phone_anki_dir(drill):
+ """The .apkg is a phone artifact, so it defaults under sync/phone/anki/."""
+ result = drill.default_output_path(Path("/home/x/projects/health/health-drill.org"))
+ assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg"
+
+
+def test_default_deck_name_is_raw_basename(drill):
+ """Deck name is the input basename with case preserved; #+TITLE is ignored."""
+ assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat"
+
+
+def test_default_deck_name_keeps_hyphens(drill):
+ """A hyphenated basename is kept verbatim rather than title-cased."""
+ assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill"