char/arbrisseau
use wasmi to download and execute arborium grammars
git clone https://git.t4t.associates/char/arbrisseau
59ec5cd
main
1//! Async Arborium WASM highlighting powered by Wasmi. 2//! 3//! Callers own grammar retrieval and integrity policy through [`ModuleLoader`]. 4//! Arbrisseau caches initialized grammars in [`Options`] and resolves injections recursively. 5 6mod runtime; 7 8use std:: collections:: HashMap ; 9use std:: error:: Error as StdError ; 10use std:: future:: Future ; 11use std:: pin:: Pin ; 12use std:: sync::{ Arc , Mutex }; 13 14use arborium_highlight::{ Injection , ParseResult }; 15use async_lock:: OnceCell ; 16 17pub use arborium_highlight:: Span ; 18 19const MAX_INJECTION_DEPTH : u32 =3 ; 20 21pub type LoaderError =Box < dyn StdError +Send +Sync +' static >; 22pub type Result < T > = std:: result:: Result < T , Error >; 23 24# [ derive ( Debug , thiserror :: Error )] 25pub enum Error { 26# [ error ( "unsupported language {language:?}" )] 27UnsupportedLanguage { language : String }, 28 29# [ error ( "failed to load {language} grammar" )] 30LoadModule { 31language : String , 32# [ source ] 33source : LoaderError , 34}, 35 36# [ error ( "failed to initialize {language} grammar" )] 37Initialize { 38language : String , 39# [ source ] 40source : wasmi:: Error , 41}, 42 43# [ error ( "failed to parse {language} source" )] 44Parse { 45language : String , 46# [ source ] 47source : wasmi:: Error , 48}, 49} 50 51/// The asynchronous result of loading a grammar's raw WebAssembly bytes. 52/// `None` means that the requested language is unsupported. 53pub type ModuleFuture < ' a > = 54Pin < Box < dyn Future < Output = std:: result:: Result < Option < Vec < u8 >>, LoaderError >> +Send +' a >>; 55 56/// Loads a grammar from a caller-defined source. 57pub type ModuleLoader =fn ( language : & str ) ->ModuleFuture ; 58 59/// Shared configuration and initialized-grammar cache for highlighting calls. 60pub struct Options { 61/// Callback used when a language's module is first needed. 62pub load_module : ModuleLoader , 63/// A cached `None` marks a language the loader reported as unsupported. 64grammars : Mutex < HashMap < String , Arc < OnceCell < Option < runtime:: Grammar >>>>>, 65} 66 67impl Options { 68/// Creates options with an initially empty in-memory grammar cache. 69pub fn new ( load_module : ModuleLoader ) ->Self { 70Self { 71 load_module, 72grammars : Mutex :: new ( HashMap :: new ()), 73} 74} 75 76async fn runtime ( & self , language : & str ) ->Result < Option < runtime:: Runtime >> { 77// Cloning the cell out releases the map lock, so unrelated languages 78// stay loadable while this one initializes. 79let cell =self 80. grammars 81. lock () 82. unwrap_or_else ( |error| error. into_inner ()) 83. entry ( language. to_owned ()) 84. or_default () 85. clone (); 86let grammar = cell 87. get_or_try_init ( ||async { 88let bytes = 89( self . load_module )( language) 90. await 91. map_err ( |source|Error :: LoadModule { 92language : language. to_owned (), 93 source, 94}) ?; 95 bytes 96. as_deref () 97. map ( runtime:: Grammar :: new) 98. transpose () 99. map_err ( |source|Error :: Initialize { 100language : language. to_owned (), 101 source, 102}) 103}) 104. await ?; 105 grammar 106. as_ref () 107. map ( runtime:: Grammar :: runtime) 108. transpose () 109. map_err ( |source|Error :: Initialize { 110language : language. to_owned (), 111 source, 112}) 113} 114} 115 116/// Reuses Wasmi instances and parser sessions across highlighting calls. 117# [ derive ( Default )] 118pub struct Highlighter { 119runtimes : HashMap < String , runtime:: Runtime >, 120} 121 122impl Highlighter { 123pub fn new () ->Self { 124Self :: default () 125} 126 127async fn parse ( 128& mut self , 129language : & str , 130source : & str , 131options : & Options , 132) ->Result < Option < ParseResult >> { 133if !self . runtimes . contains_key ( language) { 134let Some ( runtime) = options. runtime ( language). await ?else { 135return Ok ( None ); 136}; 137self . runtimes . insert ( language. to_owned (), runtime); 138} 139match self . runtimes . get_mut ( language). unwrap (). parse ( source) { 140Ok ( result) =>Ok ( Some ( result)), 141Err ( source) =>{ 142// A trap leaves guest state arbitrary, so discard the session. 143self . runtimes . remove ( language); 144Err ( Error :: Parse { 145language : language. to_owned (), 146 source, 147}) 148} 149} 150} 151 152/// Highlights a source block and recursively includes spans from injected languages. 153pub async fn highlight ( 154& mut self , 155language : & str , 156source : & str , 157options : & Options , 158) ->Result < Vec < Span >> { 159let result =self 160. parse ( language, source, options) 161. await ? 162. ok_or_else ( ||Error :: UnsupportedLanguage { 163language : language. to_owned (), 164}) ?; 165let mut spans = result. spans ; 166let mut pending =Vec :: new (); 167push_injections ( 168& mut pending, 169 source, 170 result. injections , 1710 , 172MAX_INJECTION_DEPTH , 173); 174 175while let Some ( injection) = pending. pop () { 176let Some ( result) =self 177. parse ( & injection. language , injection. text , options) 178. await ? 179else { 180continue ; 181}; 182 spans. extend ( result. spans . into_iter (). map ( |mut span|{ 183 span. start += injection. offset ; 184 span. end += injection. offset ; 185 span 186})); 187push_injections ( 188& mut pending, 189 injection. text , 190 result. injections , 191 injection. offset , 192 injection. depth , 193); 194} 195 196Ok ( spans) 197} 198} 199 200struct PendingInjection < ' a > { 201language : String , 202text : &' a str , 203offset : u32 , 204depth : u32 , 205} 206 207/// Queues injections in reverse so that popping the stack visits them in source order. 208fn push_injections < ' a >( 209pending : & mut Vec < PendingInjection < ' a >>, 210source : &' a str , 211injections : Vec < Injection >, 212offset : u32 , 213depth : u32 , 214) { 215if depth ==0 { 216return ; 217} 218 pending. extend ( injections. into_iter (). rev (). filter_map ( |injection|{ 219let text = source. get ( injection. start as usize ..injection. end as usize ) ?; 220( !text. is_empty ()). then_some ( PendingInjection { 221language : injection. language , 222 text, 223offset : offset + injection. start , 224depth : depth -1 , 225}) 226})); 227}