Slightly hacky but good enough math rendering in emacs 🪄
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

improvements

+218 -153
+50 -48
mathrender.el
··· 51 51 (defvar mathrender--binary nil 52 52 "Cached path to the mathrender executable.") 53 53 54 - (defvar-local mathrender--active-ov nil 54 + (defvar-local mathrender--active-overlay nil 55 55 "Overlay whose display is hidden while the cursor is inside it, or nil.") 56 - (defvar-local mathrender--dirty nil 56 + (defvar-local mathrender--dirty-region nil 57 57 "Cons (BEG . END) of the buffer region that changed and needs rescanning.") 58 58 59 59 ;;; Overlay helpers ··· 61 61 (defun mathrender--clear-previews () 62 62 "Remove all mathrender overlays from the current buffer." 63 63 (let ((inhibit-modification-hooks t)) 64 - (dolist (ov (overlays-in (point-min) (point-max))) 65 - (when (overlay-get ov 'mathrender-preview) 66 - (when (eq ov mathrender--active-ov) 67 - (setq mathrender--active-ov nil)) 68 - (mathrender--cancel-process ov) 69 - (delete-overlay ov))))) 64 + (dolist (overlay (overlays-in (point-min) (point-max))) 65 + (when (overlay-get overlay 'mathrender-preview) 66 + (when (eq overlay mathrender--active-overlay) 67 + (setq mathrender--active-overlay nil)) 68 + (mathrender--cancel-process overlay) 69 + (delete-overlay overlay))))) 70 70 71 71 (defun mathrender--overlay-at (pos) 72 72 "Return the mathrender overlay at POS, or nil." 73 - (cl-find-if (lambda (ov) (overlay-get ov 'mathrender-preview)) 73 + (cl-find-if (lambda (overlay) (overlay-get overlay 'mathrender-preview)) 74 74 (overlays-at pos))) 75 75 76 - (defun mathrender--make-overlay (beg end) 76 + (defun mathrender--make-overlay (begin end) 77 77 "Create a mathrender overlay from BEG to END with no display yet." 78 78 ;; front-advance=t: text inserted before the opening delimiter stays outside. 79 - (let ((ov (make-overlay beg end nil t nil)) 79 + (let ((overlay (make-overlay begin end nil t nil)) 80 80 (inhibit-modification-hooks t)) 81 - (overlay-put ov 'mathrender-preview t) 82 - (overlay-put ov 'priority 100) 83 - (overlay-put ov 'keymap 81 + (overlay-put overlay 'mathrender-preview t) 82 + (overlay-put overlay 'priority 100) 83 + (overlay-put overlay 'keymap 84 84 (let ((map (make-sparse-keymap))) 85 85 (define-key map [mouse-1] 86 86 (lambda () 87 87 (interactive) 88 88 (let ((inhibit-modification-hooks t)) 89 - (overlay-put ov 'display nil)))) 89 + (overlay-put overlay 'display nil)))) 90 90 map)) 91 - ov)) 91 + overlay)) 92 92 93 93 ;;; Math parsing 94 94 95 - (defun mathrender--parse-overlay (ov) 95 + (defun mathrender--parse-overlay (overlay) 96 96 "Return (MATH-TEXT . INLINE-P) for OV's current buffer content, or nil." 97 97 (let ((raw (buffer-substring-no-properties 98 - (overlay-start ov) (overlay-end ov)))) 98 + (overlay-start overlay) (overlay-end overlay)))) 99 99 (cond 100 100 ((string-match "\\`\\$\\$\\(\\(?:.\\|\n\\)+\\)\\$\\$\\'" raw) 101 101 (cons (string-trim (match-string 1 raw)) nil)) ··· 135 135 136 136 ;;; Async rendering 137 137 138 - (defun mathrender--cancel-process (ov) 139 - "Kill any in-flight render process attached to OV." 138 + (defun mathrender--cancel-process (overlay) 139 + "Kill any in-flight render process attached to OVERLAY." 140 140 (let ((inhibit-modification-hooks t)) 141 - (when-let ((proc (overlay-get ov 'mathrender-process))) 142 - (when (process-live-p proc) 143 - (set-process-sentinel proc nil) 144 - (when-let* ((err-buf (process-get proc 'stderr-buf)) 141 + (when-let ((process (overlay-get overlay 'mathrender-process))) 142 + (when (process-live-p process) 143 + (set-process-sentinel process nil) 144 + (when-let* ((err-buf (process-get process 'stderr-buf)) 145 145 (err-proc (and (buffer-live-p err-buf) (get-buffer-process err-buf)))) 146 146 (set-process-sentinel err-proc nil)) 147 - (delete-process proc)) 148 - (let ((out-buf (process-buffer proc)) 149 - (err-buf (process-get proc 'stderr-buf))) 147 + (delete-process process)) 148 + (let ((out-buf (process-buffer process)) 149 + (err-buf (process-get process 'stderr-buf))) 150 150 (when (buffer-live-p out-buf) 151 151 (let ((kill-buffer-query-functions nil)) 152 152 (kill-buffer out-buf))) 153 153 (when (buffer-live-p err-buf) 154 154 (let ((kill-buffer-query-functions nil)) 155 155 (kill-buffer err-buf)))) 156 - (overlay-put ov 'mathrender-process nil)))) 156 + (overlay-put overlay 'mathrender-process nil)))) 157 157 158 158 (defun mathrender--any-active-p () 159 159 "Return t if mathrender-mode is active in any buffer." ··· 161 161 (buffer-local-value 'mathrender-mode buf)) 162 162 (buffer-list))) 163 163 164 - (defun mathrender--render-overlay (ov) 165 - "Start an async render for OV; update its display when the process finishes." 166 - (when (and mathrender--binary (overlay-buffer ov)) 167 - (mathrender--cancel-process ov) 168 - (when-let ((parsed (mathrender--parse-overlay ov))) 164 + (defun mathrender--render-overlay (overlay) 165 + "Start an async render for OVERLAY; update its display when the process finishes." 166 + (when (and mathrender--binary (overlay-buffer overlay)) 167 + (mathrender--cancel-process overlay) 168 + (when-let ((parsed (mathrender--parse-overlay overlay))) 169 169 (let* ((math-text (car parsed)) 170 170 (inline-p (cdr parsed)) 171 - (target-buf (overlay-buffer ov)) 171 + (target-buf (overlay-buffer overlay)) 172 172 (font-size (with-current-buffer target-buf (mathrender--font-size-pt))) 173 173 (fg-color (with-current-buffer target-buf (mathrender--fg-color))) 174 174 (cmd `(,mathrender--binary ··· 194 194 (unwind-protect 195 195 (when (memq (process-status p) '(exit signal)) 196 196 (when (and (buffer-live-p target-buf) 197 - (overlay-buffer ov)) 197 + (overlay-buffer overlay)) 198 198 (let ((svg (when (string-prefix-p "finished" event) 199 199 (with-current-buffer out-buf (buffer-string))))) 200 200 (if (and svg (> (length svg) 0)) 201 201 (let ((img (create-image svg 'svg t :ascent 'center))) 202 - (overlay-put ov 'mathrender-image img) 203 - (overlay-put ov 'mathrender-font-size font-size) 204 - (overlay-put ov 'mathrender-color fg-color) 202 + (overlay-put overlay 'mathrender-image img) 203 + (overlay-put overlay 'mathrender-font-size font-size) 204 + (overlay-put overlay 'mathrender-color fg-color) 205 205 (with-current-buffer target-buf 206 - (unless (eq ov mathrender--active-ov) 207 - (overlay-put ov 'display img)))) 208 - (overlay-put ov 'mathrender-image nil) 206 + (unless (eq overlay mathrender--active-overlay) 207 + (overlay-put overlay 'display img)))) 208 + (overlay-put overlay 'mathrender-image nil) 209 209 (with-current-buffer target-buf 210 - (overlay-put ov 'display nil)))) 211 - (overlay-put ov 'mathrender-process nil))) 210 + (overlay-put overlay 'display nil)))) 211 + (overlay-put overlay 'mathrender-process nil))) 212 212 (let ((kill-buffer-query-functions nil)) 213 213 (when (buffer-live-p out-buf) (kill-buffer out-buf)) 214 214 (when (buffer-live-p err-buf) (kill-buffer err-buf)))))))) 215 215 (process-put proc 'stderr-buf err-buf) 216 - (overlay-put ov 'mathrender-process proc) 216 + (overlay-put overlay 'mathrender-process proc) 217 217 (process-send-string proc math-text) 218 218 (process-send-eof proc)) 219 219 (error ··· 224 224 225 225 ;;; Block detection 226 226 227 - (defun mathrender--scan (beg end) 227 + (defun mathrender--scan (begin end) 228 228 "Scan BEG..END for math blocks; create and render overlays for new ones." 229 - (let ((inhibit-modification-hooks t)) 229 + (let ((inhibit-modification-hooks t) 230 + (b (min begin end)) 231 + (e (max begin end))) 230 232 (save-excursion 231 - (goto-char beg) 233 + (goto-char b) 232 234 (let ((pairs `(("$$" "\\$\\$" nil) 233 235 (,(concat "\\" "[") ,(concat "\\\\" "]") nil) 234 236 (,(concat "\\" "(") ,(concat "\\\\" ")") t) 235 237 ("$" "\\$" t)))) 236 - (while (re-search-forward "\\$\\$\\|\\\\\\[\\|\\\\(\\|\\$" end t) 238 + (while (re-search-forward "\\$\\$\\|\\\\\\[\\|\\\\(\\|\\$" e t) 237 239 (let* ((opener (match-string-no-properties 0)) 238 240 (full-beg (match-beginning 0)) 239 241 (math-beg (match-end 0))
+168 -105
src/main.rs
··· 1 1 use std::io::{self, Read}; 2 - use std::panic; 3 2 use std::path::PathBuf; 3 + use std::sync::OnceLock; 4 4 5 5 use chrono::Datelike; 6 6 use clap::Parser; ··· 30 30 color: String, 31 31 } 32 32 33 - /// tex2typst-rs 0.1 panics on unsupported tokens like \begin{...}. 34 - fn safe_tex2typst(s: &str) -> String { 35 - if s.trim().is_empty() { 36 - return s.to_owned(); 33 + #[derive(Debug)] 34 + enum Error { 35 + Compile(String), 36 + NoPages, 37 + } 38 + 39 + impl std::fmt::Display for Error { 40 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 + match self { 42 + Error::Compile(msg) => write!(f, "Typst compile error(s):\n {}", msg), 43 + Error::NoPages => write!(f, "Typst produced no pages"), 44 + } 37 45 } 38 - match panic::catch_unwind(|| tex2typst_rs::tex2typst(s)) { 39 - Ok(r) => r, 40 - Err(_) => s.to_owned(), 46 + } 47 + 48 + impl std::error::Error for Error {} 49 + 50 + struct GlobalFonts { 51 + book: LazyHash<FontBook>, 52 + fonts: Vec<Font>, 53 + } 54 + 55 + fn get_global_fonts() -> &'static GlobalFonts { 56 + static FONTS: OnceLock<GlobalFonts> = OnceLock::new(); 57 + FONTS.get_or_init(|| { 58 + let mut book = FontBook::new(); 59 + let mut fonts = Vec::new(); 60 + 61 + for font_data in typst_assets::fonts() { 62 + let bytes = Bytes::new(font_data); 63 + for index in 0u32.. { 64 + match Font::new(bytes.clone(), index) { 65 + Some(font) => { 66 + book.push(font.info().clone()); 67 + fonts.push(font); 68 + } 69 + None => break, 70 + } 71 + } 72 + } 73 + GlobalFonts { book: LazyHash::new(book), fonts } 74 + }) 75 + } 76 + 77 + fn safe_tex2typst(s: &str, buf: &mut String) { 78 + let s = s.trim(); 79 + if s.is_empty() { 80 + return; 41 81 } 82 + // Assertion enforcing pre-condition bounds 83 + debug_assert!(s.len() < 10000, "Input to safe_tex2typst is abnormally large"); 84 + 85 + buf.push_str(&tex2typst_rs::tex2typst(s)); 42 86 } 43 87 44 - fn convert_matrix_env(env: &str, content: &str) -> String { 88 + fn env_matrix_convert(env: &str, content: &str, out_typst_string: &mut String) { 45 89 let delim = match env { 46 90 "pmatrix" | "pmatrix*" => "(", 47 91 "bmatrix" | "bmatrix*" => "[", ··· 53 97 _ => "(", 54 98 }; 55 99 56 - let rows: Vec<Vec<String>> = content 57 - .split("\\\\") 58 - .map(|row| { 59 - row.split('&') 60 - .map(|cell| safe_tex2typst(cell.trim())) 61 - .collect() 62 - }) 63 - .filter(|row: &Vec<String>| !row.is_empty() && !row[0].is_empty()) 64 - .collect(); 100 + match delim { 101 + "" => out_typst_string.push_str("mat(delim: #none, "), 102 + "(" => out_typst_string.push_str("mat("), 103 + d => { out_typst_string.push_str("mat(delim: \""); out_typst_string.push_str(d); out_typst_string.push_str("\", "); } 104 + } 105 + 106 + let mut first_row = true; 107 + for row in content.split("\\\\").take(256) { 108 + if row.trim().is_empty() { continue; } 109 + if !first_row { out_typst_string.push_str("; "); } 110 + first_row = false; 65 111 66 - let mat_content = rows.iter() 67 - .map(|row| row.join(", ")) 68 - .collect::<Vec<_>>() 69 - .join("; "); 112 + let mut first_cell = true; 113 + for cell in row.split('&').take(256) { 114 + let cell = cell.trim(); 115 + if !first_cell { out_typst_string.push_str(", "); } 116 + first_cell = false; 117 + safe_tex2typst(cell, out_typst_string); 118 + } 119 + } 120 + out_typst_string.push(')'); 121 + } 122 + 123 + fn env_cases_convert(content: &str, out_typst_string: &mut String) { 124 + out_typst_string.push_str("cases("); 125 + let mut first_row = true; 126 + for row in content.split("\\\\").take(256) { 127 + let row = row.trim(); 128 + if row.is_empty() { continue; } 129 + if !first_row { out_typst_string.push_str(", "); } 130 + first_row = false; 131 + 132 + let mut parts = row.splitn(2, '&'); 133 + let expr = parts.next().expect("Cases row must have content").trim(); 134 + let cond = parts.next().map(|s| s.trim()); 135 + 136 + safe_tex2typst(expr, out_typst_string); 137 + if let Some(c) = cond { 138 + out_typst_string.push_str(", "); 139 + safe_tex2typst(c, out_typst_string); 140 + } 141 + } 142 + out_typst_string.push(')'); 143 + } 144 + 145 + fn process_latex_env(env: &str, content: &str, result: &mut String) { 146 + debug_assert!(!env.is_empty(), "Environment string cannot be empty"); 147 + 148 + match env { 149 + "pmatrix" | "pmatrix*" | 150 + "bmatrix" | "bmatrix*" | 151 + "vmatrix" | "vmatrix*" | 152 + "Bmatrix" | "Bmatrix*" | 153 + "Vmatrix" | "Vmatrix*" | 154 + "matrix" | "matrix*" | 155 + "smallmatrix" => env_matrix_convert(env, content, result), 156 + 157 + "cases" | "dcases" => env_cases_convert(content, result), 158 + 159 + "align" | "align*" | 160 + "aligned" | "alignat" | "alignat*" | 161 + "gather" | "gather*" | 162 + "multline" | "multline*" | 163 + "split" | "equation" | "equation*" => safe_tex2typst(content, result), 70 164 71 - match delim { 72 - "" => format!("mat(delim: #none, {mat_content})"), 73 - "(" => format!("mat({mat_content})"), 74 - d => format!("mat(delim: \"{d}\", {mat_content})"), 165 + _ => { 166 + result.push_str("\\begin{"); 167 + result.push_str(env); 168 + result.push('}'); 169 + result.push_str(content); 170 + result.push_str("\\end{"); 171 + result.push_str(env); 172 + result.push('}'); 173 + } 75 174 } 76 175 } 77 176 78 - fn convert_cases_env(content: &str) -> String { 79 - let rows: Vec<String> = content 80 - .split("\\\\") 81 - .filter_map(|row| { 82 - let row = row.trim(); 83 - if row.is_empty() { return None; } 84 - let parts: Vec<&str> = row.splitn(2, '&').collect(); 85 - Some(match parts.as_slice() { 86 - [expr, cond] => format!("{} {}", 87 - safe_tex2typst(expr.trim()), 88 - safe_tex2typst(cond.trim())), 89 - [expr] => safe_tex2typst(expr.trim()), 90 - _ => safe_tex2typst(row), 91 - }) 92 - }) 93 - .collect(); 94 - format!("cases({})", rows.join(", ")) 177 + fn find_env_end<'a>(rest: &'a str, env: &str) -> Option<(&'a str, &'a str)> { 178 + let end_search = "\\end{"; 179 + let mut search_rest = rest; 180 + let mut current_offset = 0; 181 + 182 + while let Some(end_begin) = search_rest.find(end_search) { 183 + let tag_content_start = end_begin + end_search.len(); 184 + if search_rest[tag_content_start..].starts_with(env) && 185 + search_rest[tag_content_start + env.len()..].starts_with('}') { 186 + let content = &rest[..current_offset + end_begin]; 187 + let advance = current_offset + end_begin + end_search.len() + env.len() + 1; 188 + return Some((content, &rest[advance..])); 189 + } 190 + let advance = end_begin + end_search.len(); 191 + search_rest = &search_rest[advance..]; 192 + current_offset += advance; 193 + } 194 + None 95 195 } 96 196 97 - /// tex2typst-rs panics on \begin{...} environments; we intercept and convert 98 - /// them to Typst equivalents before calling the library. 99 197 fn latex_to_typst(input: &str) -> String { 100 - let mut result = String::new(); 198 + let mut result = String::with_capacity(input.len() * 2); 101 199 let mut rest = input; 102 200 103 201 while let Some(begin_pos) = rest.find("\\begin{") { 104 - result.push_str(&safe_tex2typst(&rest[..begin_pos])); 202 + safe_tex2typst(&rest[..begin_pos], &mut result); 105 203 rest = &rest[begin_pos + 7..]; 106 204 107 205 let Some(brace_end) = rest.find('}') else { ··· 111 209 let env = &rest[..brace_end]; 112 210 rest = &rest[brace_end + 1..]; 113 211 114 - let end_tag = format!("\\end{{{env}}}"); 115 - let Some(end_pos) = rest.find(end_tag.as_str()) else { 116 - result.push_str(&format!("\\begin{{{env}}}")); 117 - continue; 118 - }; 119 - let content = &rest[..end_pos]; 120 - rest = &rest[end_pos + end_tag.len()..]; 121 - 122 - let converted = match env { 123 - "pmatrix" | "pmatrix*" | 124 - "bmatrix" | "bmatrix*" | 125 - "vmatrix" | "vmatrix*" | 126 - "Bmatrix" | "Bmatrix*" | 127 - "Vmatrix" | "Vmatrix*" | 128 - "matrix" | "matrix*" | 129 - "smallmatrix" => convert_matrix_env(env, content), 130 - 131 - "cases" | "dcases" => convert_cases_env(content), 132 - 133 - "align" | "align*" | 134 - "aligned" | "alignat" | "alignat*" | 135 - "gather" | "gather*" | 136 - "multline" | "multline*" | 137 - "split" | "equation" | "equation*" => safe_tex2typst(content), 138 - 139 - _ => format!("\\begin{{{env}}}{content}\\end{{{env}}}"), 140 - }; 141 - result.push_str(&converted); 212 + if let Some((content, new_rest)) = find_env_end(rest, env) { 213 + process_latex_env(env, content, &mut result); 214 + rest = new_rest; 215 + } else { 216 + result.push_str("\\begin{"); 217 + result.push_str(env); 218 + result.push('}'); 219 + } 142 220 } 143 221 144 - result.push_str(&safe_tex2typst(rest)); 222 + safe_tex2typst(rest, &mut result); 145 223 result 146 224 } 147 225 226 + fn get_global_library() -> &'static LazyHash<Library> { 227 + static LIBRARY: OnceLock<LazyHash<Library>> = OnceLock::new(); 228 + LIBRARY.get_or_init(|| { 229 + LazyHash::new(typst_library::LibraryBuilder::default().build()) 230 + }) 231 + } 232 + 148 233 struct MathWorld { 149 - library: LazyHash<Library>, 150 - book: LazyHash<FontBook>, 151 - fonts: Vec<Font>, 152 - source: Source, 234 + source: Source, 153 235 } 154 236 155 237 impl MathWorld { ··· 157 239 let main_id = FileId::new(None, VirtualPath::new("/main.typ")); 158 240 let source = Source::new(main_id, source_text); 159 241 160 - let mut book = FontBook::new(); 161 - let mut fonts = Vec::new(); 162 - 163 - for font_data in typst_assets::fonts() { 164 - let bytes = Bytes::new(font_data); 165 - for index in 0u32.. { 166 - match Font::new(bytes.clone(), index) { 167 - Some(font) => { 168 - book.push(font.info().clone()); 169 - fonts.push(font); 170 - } 171 - None => break, 172 - } 173 - } 174 - } 175 - 176 242 Self { 177 - library: LazyHash::new(typst_library::LibraryBuilder::default().build()), 178 - book: LazyHash::new(book), 179 - fonts, 180 243 source, 181 244 } 182 245 } 183 246 } 184 247 185 248 impl World for MathWorld { 186 - fn library(&self) -> &LazyHash<Library> { &self.library } 187 - fn book(&self) -> &LazyHash<FontBook> { &self.book } 249 + fn library(&self) -> &LazyHash<Library> { get_global_library() } 250 + fn book(&self) -> &LazyHash<FontBook> { &get_global_fonts().book } 188 251 fn main(&self) -> FileId { self.source.id() } 189 252 190 253 fn source(&self, id: FileId) -> FileResult<Source> { ··· 200 263 } 201 264 202 265 fn font(&self, index: usize) -> Option<Font> { 203 - self.fonts.get(index).cloned() 266 + get_global_fonts().fonts.get(index).cloned() 204 267 } 205 268 206 269 fn today(&self, offset: Option<i64>) -> Option<Datetime> { ··· 231 294 display: bool, 232 295 font_size: f64, 233 296 color: &str, 234 - ) -> Result<String, Box<dyn std::error::Error>> { 297 + ) -> Result<String, Error> { 235 298 236 299 let typst_math = if is_typst { 237 300 input.to_owned() ··· 245 308 246 309 let document: PagedDocument = compiled.output.map_err(|errors| { 247 310 let msgs: Vec<String> = errors.iter().map(|e| e.message.to_string()).collect(); 248 - format!("Typst compile error(s):\n {}", msgs.join("\n ")) 311 + Error::Compile(msgs.join("\n ")) 249 312 })?; 250 313 251 - let page = document.pages.first().ok_or("Typst produced no pages")?; 314 + let page = document.pages.first().ok_or(Error::NoPages)?; 252 315 Ok(typst_svg::svg(page)) 253 316 } 254 317