diff --git a/CHANGELOG.md b/CHANGELOG.md index c98c0450..6cf2034f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased * When strip=True in code/text, then text steps line are rstripped +* "typst" extension for writing mathematical expressions # v0.21.2 diff --git a/nelsie/python/nelsie/box.py b/nelsie/python/nelsie/box.py index 872e56bf..07140c30 100644 --- a/nelsie/python/nelsie/box.py +++ b/nelsie/python/nelsie/box.py @@ -6,6 +6,7 @@ ImageContent, check_image_path_or_data, normalize_and_watch_image_path, + normalize_image_path, ) from .steps import ( Sn, @@ -228,12 +229,16 @@ def image( *, enable_steps: Sv[bool] = True, shift_steps: int = 0, + watch=True, **box_args, ): sn_check(path_or_data, check_image_path_or_data) sv_check(enable_steps, check_is_bool) sv_check(shift_steps, check_is_int) - path_or_data = sn_map(path_or_data, normalize_and_watch_image_path) + if watch: + path_or_data = sn_map(path_or_data, normalize_and_watch_image_path) + else: + path_or_data = sn_map(path_or_data, normalize_image_path) box = self.box(**box_args) box._content = ImageContent(path_or_data, enable_steps, shift_steps) return box diff --git a/nelsie/python/nelsie/helpers/typst.py b/nelsie/python/nelsie/helpers/typst.py new file mode 100644 index 00000000..1e44962d --- /dev/null +++ b/nelsie/python/nelsie/helpers/typst.py @@ -0,0 +1,54 @@ +import subprocess +import hashlib +import os + + +class Typst: + def __init__(self, typst_path="typst", cache_path="./typst_cache"): + cache_path = os.path.abspath(cache_path) + if not os.path.isdir(cache_path): + os.makedirs(cache_path) + self.typst_path = typst_path + self.cache_path = cache_path + + self.default_header = self.get_header() + self.version = self._call_typst(["--version"]) + hasher = hashlib.sha1() + hasher.update(self.version) + self.hasher = hasher + self.generated = set() + + def _call_typst(self, args: list[str]): + r = subprocess.run([self.typst_path, *args], check=True, stdout=subprocess.PIPE) + return r.stdout + + def get_header(self, width="auto", height="auto", margin_x="0pt", margin_y="0pt"): + return f"#set page(width: {width}, height: {height}, margin: (x: {margin_x}, y: {margin_y}))\n" + + def get_path(self, text: str, use_header: bool = True): + if use_header: + text = f"{self.default_header}\n{text}" + hasher = self.hasher.copy() + hasher.update(text.encode()) + hex = hasher.digest().hex() + self.generated.add(hex) + output_path = os.path.join(self.cache_path, f"{hex}.svg") + if os.path.isfile(output_path): + return output_path + input_path = os.path.join(self.cache_path, f"{hex}.typ") + with open(input_path, "w") as f: + f.write(text) + self._call_typst(["compile", "--format=svg", input_path]) + return output_path + + def render(self, slide, text: str, use_header: bool = True, **kwargs): + kwargs.setdefault("watch", False) + slide.image(self.get_path(text, use_header), **kwargs) + + def clean_cache(self): + for path in os.listdir(self.cache_path): + if not path.endswith(".typ") and not path.endswith(".svg"): + continue + base = path[:-4] + if base not in self.generated: + os.unlink(os.path.join(self.cache_path, path)) diff --git a/nelsie/python/nelsie/image.py b/nelsie/python/nelsie/image.py index 05ca02a9..8d11ae1b 100644 --- a/nelsie/python/nelsie/image.py +++ b/nelsie/python/nelsie/image.py @@ -48,6 +48,12 @@ def normalize_and_watch_image_path(path): return path +def normalize_image_path(path): + if isinstance(path, str): + path = os.path.abspath(path) + return path + + def _put_into_shared_data( path_or_data: PathOrImageData | None, content, shared_data, steps ):