forks/sorcery

git clone https://git.gae.moe/forks/sorcery

Charlotte Somperf: use arbrisseau binary cache90f94f2

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