diff --git a/README.md b/README.md index cee2061..8029b02 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ To render the flashcard in your document as well add some code like this ``` - Add a comment like `// ANKI: MY::DECK` to your document to set a deck used for all flashcards after this comment (You can use multiple decks per file) +- Add a file named `.anki` containing a deck name to define a default deck on a directory base - Add a file named `.anki.typ` to define a preamble on a directory base. You can find the default preamble [here](./src/anki/typst_compiler.py). - Tip: Despite the use of SVGs you can still search your flashcards in Anki as the typst source is added into an invisible html paragraph diff --git a/pyproject.toml b/pyproject.toml index 150a000..ddae3f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "pdm.backend" [project] name = "typstar" -version = "1.1.1" +version = "1.2.0" description = "Neovim plugin for efficient note taking in Typst" authors = [ { name = "arne314" } diff --git a/src/anki/config_parser.py b/src/anki/config_parser.py new file mode 100644 index 0000000..a821d13 --- /dev/null +++ b/src/anki/config_parser.py @@ -0,0 +1,35 @@ +from collections import defaultdict +from functools import cache +from glob import glob +from pathlib import Path + + +class RecursiveConfigParser: + dir: Path + targets: set[str] + results: dict[str, dict[Path, str]] + + def __init__(self, dir, targets): + self.dir = dir + self.targets = set(targets) + self.results = defaultdict(dict) + self._parse_recursive() + + def _parse_recursive(self): + files = [] + for target in self.targets: + files.extend(glob(f"{self.dir}/**/{target}", include_hidden=target.startswith("."), recursive=True)) + for file in files: + file = Path(file) + if file.name in self.targets: + self.results[file.name][file.parent] = file.read_text(encoding="utf-8") + + @cache + def get_config(self, path: Path, target) -> str | None: + root_parent = self.dir.parent.resolve() + path = Path(path.resolve()) + target_results = self.results[target] + while path != root_parent: + if result := target_results.get(path): + return result + path = path.parent diff --git a/src/anki/parser.py b/src/anki/parser.py index dd33158..ba49abf 100644 --- a/src/anki/parser.py +++ b/src/anki/parser.py @@ -1,7 +1,6 @@ -import glob import json import re -from functools import cache +from glob import glob from pathlib import Path from typing import List, Tuple @@ -9,6 +8,7 @@ import appdirs import tree_sitter from tree_sitter_typst import language as get_typst_language +from .config_parser import RecursiveConfigParser from .file_handler import FileHandler from .flashcard import Flashcard @@ -38,6 +38,8 @@ ts_deck_query = """ deck_regex = re.compile(r"\W+ANKI:\s*([\S ]*)") + + class FlashcardParser: typst_language: tree_sitter.Language typst_parser: tree_sitter.Parser @@ -56,7 +58,7 @@ class FlashcardParser: self.file_handlers = [] self._load_file_hashes() - def _parse_file(self, file: FileHandler, preamble: str | None) -> List[Flashcard]: + def _parse_file(self, file: FileHandler, preamble: str | None, default_deck: str | None) -> List[Flashcard]: cards = [] tree = self.typst_parser.parse(file.get_bytes(), encoding="utf8") card_captures = self.flashcard_query.captures(tree.root_node) @@ -73,7 +75,7 @@ class FlashcardParser: deck_refs: List[Tuple[int, str | None]] = [] deck_refs_idx = -1 - current_deck = None + current_deck = default_deck if deck_captures: deck_captures["deck"].sort(key=row_compare) for comment in deck_captures["deck"]: @@ -108,6 +110,7 @@ class FlashcardParser: return cards def parse_directory(self, root_dir: Path, force_scan: Path | None = None): + flashcards = [] single_file = None is_force_scan = force_scan is not None if is_force_scan: @@ -123,22 +126,9 @@ class FlashcardParser: f"Parsing flashcards in {scan_dir if single_file is None else single_file} ...", flush=True, ) - preambles = {} - flashcards = [] + configs = RecursiveConfigParser(root_dir, {".anki", ".anki.typ"}) - @cache - def get_preamble(path: Path) -> str | None: - while path != root_dir.parent: - if preamble := preambles.get(path): - return preamble - path = path.parent - - for file in glob.glob(f"{root_dir}/**/.anki.typ", include_hidden=True, recursive=True): - file = Path(file) - if file.name == ".anki.typ": - preambles[file.parent] = file.read_text(encoding="utf-8") - - for file in glob.glob(f"{scan_dir}/**/**.typ", recursive=True): + for file in glob(f"{scan_dir}/**/**.typ", recursive=True): file = Path(file) if single_file is not None and file != single_file: continue @@ -146,7 +136,7 @@ class FlashcardParser: fh = FileHandler(file) file_changed = self._hash_changed(fh) if is_force_scan or file_changed: - cards = self._parse_file(fh, get_preamble(file.parent)) + cards = self._parse_file(fh, configs.get_config(file, ".anki.typ"), configs.get_config(file, ".anki")) self.file_handlers.append((fh, cards)) flashcards.extend(cards) return flashcards diff --git a/uv.lock b/uv.lock index c2e72a0..8aed491 100644 --- a/uv.lock +++ b/uv.lock @@ -437,7 +437,7 @@ wheels = [ [[package]] name = "typstar" -version = "1.1.0" +version = "1.2.0" source = { editable = "." } dependencies = [ { name = "aiohttp" },