forks/sorcery
git clone https://git.gae.moe/forks/sorcery
90f94f2
typst_support
1use std:: fs; 2use std:: io:: Read as _; 3use std:: path::{ Path , PathBuf }; 4use std:: sync::{ Arc , OnceLock }; 5 6use anyhow::{ Context , Result }; 7use arborium_highlight::{ HtmlFormat , Span , html_escape, spans_to_flat_tokens, spans_to_html}; 8use sha2::{ Digest , Sha256 }; 9 10use crate :: grammar_manifest; 11 12static CACHE_DIR : OnceLock < PathBuf > =OnceLock :: new (); 13 14pub struct Cache { 15options : arbrisseau:: Options , 16} 17 18impl Cache { 19pub fn new ( root : impl AsRef < Path >) ->Self { 20let dir = root. as_ref (). join ( grammar_manifest:: VERSION ); 21CACHE_DIR . set ( dir. clone ()). expect ( "grammar cache already initialized" ); 22Self { 23options : arbrisseau:: Options :: new ( load_module). with_compiled_cache ( dir), 24} 25} 26} 27 28fn load_module ( language : & str ) -> arbrisseau:: ModuleFuture < ' _ > { 29Box :: pin ( async move { 30let Some (( size, hash)) = grammar_manifest:: artifact ( language) else { 31return Ok ( None ); 32}; 33let path =CACHE_DIR 34. get () 35. expect ( "grammar cache not initialized" ) 36. join ( format! ( "{language}.wasm" )); 37if let Ok ( bytes) = fs:: read ( & path) 38 &&verified ( & bytes, size, hash) 39{ 40return Ok ( Some ( bytes)); 41} 42match download ( language, size, hash, & path) { 43Ok ( bytes) =>Ok ( Some ( bytes)), 44Err ( error) =>{ 45eprintln! ( "sorceryd: cannot load {language} grammar: {error:#}" ); 46Ok ( None ) 47} 48} 49}) 50} 51 52fn verified ( bytes : & [ u8 ], size : usize , hash : & str ) ->bool { 53 bytes. len () == size &&format! ( "{:x}" , Sha256 :: digest ( bytes)) == hash 54} 55 56fn download ( language : & str , size : usize , hash : & str , path : & Path ) ->Result < Vec < u8 >> { 57let url =format! ( 58"https://cdn.jsdelivr.net/npm/@arborium/{language}@{}/grammar_bg.wasm" , 59 grammar_manifest:: VERSION , 60); 61let mut response = minreq:: get ( & url) 62. with_timeout ( 30 ) 63. with_max_redirects ( 3 ) 64. send_lazy () 65. with_context ( ||format! ( "downloading {url}" )) ?; 66if response. status_code !=200 { 67 anyhow:: bail!( "downloading {url}: HTTP {}" , response. status_code ); 68} 69let mut bytes =Vec :: with_capacity ( size); 70 std:: io:: Read :: take ( & mut response, sizeas u64 +1 ) 71. read_to_end ( & mut bytes) 72. with_context ( ||format! ( "downloading {url}" )) ?; 73if !verified ( & bytes, size, hash) { 74 anyhow:: bail!( "downloading {url}: size or SHA-256 mismatch" ); 75} 76let dir = path. parent (). unwrap (); 77 fs:: create_dir_all ( dir). with_context ( ||format! ( "creating grammar cache {}" , dir. display ())) ?; 78// Concurrent sorcery processes may race on the same cache; a per-process 79// temporary and rename keep the final file always complete. 80let temporary = path. with_extension ( format! ( "wasm.tmp-{}" , std:: process:: id ())); 81 fs:: write ( & temporary, & bytes) 82. and_then ( |() | fs:: rename ( & temporary, path)) 83. with_context ( ||format! ( "installing grammar cache {}" , path. display ())) ?; 84Ok ( bytes) 85} 86 87pub struct Highlighter { 88cache : Arc < Cache >, 89highlighter : arbrisseau:: Highlighter , 90} 91 92impl Highlighter { 93pub fn new ( cache : Arc < Cache >) ->Self { 94Self { 95 cache, 96highlighter : arbrisseau:: Highlighter :: new (), 97} 98} 99 100pub fn highlight ( & mut self , language : & str , source : & str ) ->Result < String > { 101let spans = pollster:: block_on ( self . highlighter . highlight ( 102 language, 103 source, 104& self . cache . options , 105)) ?; 106Ok ( spans_to_html ( source, spans, & HtmlFormat :: default ())) 107} 108 109pub fn highlight_lines ( & mut self , language : & str , source : & str ) ->Result < Vec < String >> { 110let spans = pollster:: block_on ( self . highlighter . highlight ( 111 language, 112 source, 113& self . cache . options , 114)) ?; 115Ok ( render_lines ( source, spans)) 116} 117} 118 119fn render_lines ( source : & str , spans : Vec < Span >) ->Vec < String > { 120if source. is_empty () { 121return Vec :: new (); 122} 123let tokens =spans_to_flat_tokens ( source, spans); 124let mut token =0 ; 125let mut offset =0 ; 126let mut lines = source. split ( '\n' ). collect ::< Vec < _ >>(); 127if source. ends_with ( '\n' ) { 128 lines. pop (); 129} 130 131 lines 132. into_iter () 133. map ( |raw_line|{ 134let line = raw_line. strip_suffix ( '\r' ). unwrap_or ( raw_line); 135let start = offset; 136let end = start + line. len (); 137 offset += raw_line. len () +1 ; 138while tokens 139. get ( token) 140. is_some_and ( |span| span. end as usize < = start) 141{ 142 token +=1 ; 143} 144 145let mut html =String :: new (); 146let mut position = start; 147let mut i = token; 148while let Some ( span) = tokens. get ( i). filter ( |span|( span. start as usize ) < end) { 149let span_start =( span. start as usize ). max ( start); 150let span_end =( span. end as usize ). min ( end); 151if position < span_start{ 152 html. push_str ( & html_escape ( & source[ position..span_start])); 153} 154if span_start < span_end{ 155 html. push_str ( & format! ( 156"<a-{tag}>{text}</a-{tag}>" , 157 tag = span. tag , 158 text =html_escape ( & source[ span_start..span_end]), 159)); 160} 161 position = span_end; 162 i +=1 ; 163} 164if position < end{ 165 html. push_str ( & html_escape ( & source[ position..end])); 166} 167 html 168}) 169. collect () 170} 171 172# [ cfg ( test )] 173mod tests{ 174use arbrisseau:: Span ; 175 176use super :: render_lines; 177 178# [ test ] 179fn renders_highlighted_lines_independently () { 180let source ="/* <\n * x */\r\nlet x = 1;\n" ; 181let spans =vec! [ 182Span { 183start : 0 , 184end : 12 , 185capture : "comment" . into (), 186pattern_index : 0 , 187}, 188Span { 189start : 14 , 190end : 17 , 191capture : "keyword" . into (), 192pattern_index : 0 , 193}, 194]; 195assert_eq! ( 196render_lines ( source, spans), 197[ 198"<a-c>/* <</a-c>" , 199"<a-c> * x */</a-c>" , 200"<a-k>let</a-k> x = 1;" 201] 202); 203} 204} 205