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