syntastica_js/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(rust_2018_idioms, missing_docs)]
3#![deny(unsafe_op_in_unsafe_fn)]
4
5use std::{
6    borrow::Cow,
7    collections::{BTreeMap, HashMap},
8    ffi::{c_char, c_void, CStr},
9    marker::PhantomData,
10    str::FromStr,
11    sync::{LazyLock, Mutex},
12};
13
14use syntastica::{
15    language_set::{FileType, HighlightConfiguration, Language, LanguageSet, SupportedLanguage},
16    renderer::{HtmlRenderer, TerminalRenderer},
17    style::{Color, Style},
18    theme::ResolvedTheme,
19    Highlights, Processor,
20};
21
22extern "C" {
23    fn malloc(size: usize) -> *mut c_void;
24    fn memcpy(dest: *mut c_void, src: *const c_void, count: usize) -> *mut c_void;
25}
26
27fn alloc_string(str: &str) -> *const c_char {
28    let ptr = unsafe { malloc(str.len() + 1) };
29    unsafe { memcpy(ptr, str.as_ptr() as *const _, str.len()) };
30    unsafe { (ptr as *mut c_char).add(str.len()).write(0) };
31    ptr as *const _
32}
33
34macro_rules! bail {
35    ($errmsg:ident, $($msg:tt)+) => {{
36        unsafe { *$errmsg = alloc_string(&format!($($msg)*)) };
37        return std::ptr::null();
38    }};
39}
40
41static LANGUAGES: LazyLock<LangSet> = LazyLock::new(LangSet::default);
42static PROCESSOR: LazyLock<Mutex<Processor<'static, LangSet>>> =
43    LazyLock::new(|| Mutex::new(Processor::new(&*LANGUAGES)));
44
45syntastica_macros::js_lang_info!();
46
47type _Highlights = Vec<Vec<(String, Option<String>)>>;
48
49#[derive(serde::Deserialize)]
50#[serde(rename_all = "camelCase")]
51enum Theme {
52    Builtin(String),
53    Custom(BTreeMap<Cow<'static, str>, Style>),
54}
55
56struct Lang<'set>(&'static str, PhantomData<&'set ()>);
57
58#[derive(Default)]
59struct LangSet {
60    languages: Mutex<HashMap<&'static str, (&'static HighlightConfiguration, Vec<FileType>)>>,
61}
62
63impl LangSet {
64    fn add_lang(&self, info: &'static LangInfo) {
65        fn cptr_to_str(str: *const c_char) -> &'static str {
66            unsafe { CStr::from_ptr(str).to_str().unwrap() }
67        }
68
69        let name = cptr_to_str(info.name);
70        let mut config = HighlightConfiguration::new(
71            unsafe { (&info.language as *const Language).read() },
72            name,
73            cptr_to_str(info.highlights_query),
74            cptr_to_str(info.injections_query),
75            cptr_to_str(info.locals_query),
76        )
77        .unwrap();
78        config.configure(syntastica::theme::THEME_KEYS);
79        self.languages.lock().unwrap().insert(
80            name,
81            (
82                Box::leak(Box::new(config)),
83                unsafe { std::slice::from_raw_parts(info.file_types, info.file_types_len) }
84                    .iter()
85                    .map(|&ft| FileType::from_str(cptr_to_str(ft)).unwrap())
86                    .collect(),
87            ),
88        );
89    }
90}
91
92impl<'s> LanguageSet<'s> for LangSet {
93    type Language = Lang<'s>;
94
95    fn get_language(
96        &self,
97        language: Self::Language,
98    ) -> syntastica::Result<&HighlightConfiguration> {
99        Ok(self
100            .languages
101            .lock()
102            .unwrap()
103            .get(language.0)
104            .expect("if a Lang instance exists, it should be a loaded language")
105            .0)
106    }
107}
108
109impl<'set> SupportedLanguage<'set, LangSet> for Lang<'set> {
110    fn name(&self) -> Cow<'_, str> {
111        self.0.into()
112    }
113
114    fn for_name(name: impl AsRef<str>, set: &'set LangSet) -> syntastica::Result<Self> {
115        let name = name.as_ref();
116        set.languages
117            .lock()
118            .unwrap()
119            .get_key_value(name)
120            .map(|(&name, _)| Self(name, PhantomData))
121            .ok_or_else(|| syntastica::Error::UnsupportedLanguage(name.to_string()))
122    }
123
124    fn for_file_type(_file_type: FileType, _set: &'set LangSet) -> Option<Self> {
125        None
126    }
127}
128
129unsafe fn string_from_ptr(ptr: *const c_char) -> String {
130    unsafe { CStr::from_ptr(ptr) }
131        .to_string_lossy()
132        .into_owned()
133}
134
135/// Add a language to the set.
136///
137/// # Safety
138///
139/// The `info` param must point to a valid `LangInfo` struct with the `'static` lifetime.
140#[no_mangle]
141pub unsafe fn add_language(info: *mut LangInfo) {
142    LANGUAGES.add_lang(unsafe { &*info });
143}
144
145/// Process and render a piece of code in the given language with the given theme.
146///
147/// # Safety
148///
149/// All parameters must be valid pointers.
150#[no_mangle]
151pub unsafe fn highlight(
152    errmsg: *mut *const c_char,
153    code: *const c_char,
154    language: *const c_char,
155    theme: *const c_char,
156    renderer: *const c_char,
157) -> *const c_char {
158    let code = unsafe { string_from_ptr(code) };
159    let language = unsafe { string_from_ptr(language) };
160    let theme = unsafe { string_from_ptr(theme) };
161    let renderer = unsafe { string_from_ptr(renderer) };
162
163    let Ok(language) = Lang::for_name(&language, &*LANGUAGES) else {
164        bail!(errmsg, "unsupported language '{language}'");
165    };
166
167    let theme = match serde_json::from_str::<Theme>(&theme) {
168        Ok(Theme::Builtin(theme)) => match syntastica_themes::from_str(&theme) {
169            Some(theme) => theme,
170            None => bail!(errmsg, "unknown builtin theme '{theme}'"),
171        },
172        Ok(Theme::Custom(theme)) => ResolvedTheme::new(theme),
173        Err(err) => bail!(errmsg, "theme is invalid JSON: {err}"),
174    };
175
176    let highlights = match PROCESSOR.lock().unwrap().process(&code, language) {
177        Ok(highlights) => highlights,
178        Err(err) => bail!(errmsg, "processing failed: {err}"),
179    };
180
181    _render(errmsg, &highlights, &renderer, theme)
182}
183
184/// Process a piece of code in the given language, and return the [`Highlights`] for a following
185/// call to [`render`].
186///
187/// # Safety
188///
189/// All parameters must be valid pointers.
190#[no_mangle]
191pub unsafe fn process(
192    errmsg: *mut *const c_char,
193    code: *const c_char,
194    language: *const c_char,
195) -> *const c_char {
196    let code = unsafe { string_from_ptr(code) };
197    let language = unsafe { string_from_ptr(language) };
198
199    let Ok(language) = Lang::for_name(&language, &*LANGUAGES) else {
200        bail!(errmsg, "unsupported language '{language}'");
201    };
202
203    let highlights = match PROCESSOR.lock().unwrap().process(&code, language) {
204        Ok(highlights) => highlights,
205        Err(err) => bail!(errmsg, "processing failed: {err}"),
206    };
207
208    match serde_json::to_string(&highlights) {
209        Ok(highlights) => alloc_string(&highlights),
210        Err(err) => bail!(errmsg, "failed serializing highlights: {err}"),
211    }
212}
213
214/// Render code that was previously processed by calling [`process`] given the name of a
215/// [`Renderer`](syntastica::renderer::Renderer).
216///
217/// The renderer name is either `HTML` or `Terminal` in any casing. To specify a background color
218/// for the terminal renderer, append a hex color literal like `terminal#282828` or `Terminal#fff`.
219///
220/// # Safety
221///
222/// All parameters must be valid pointers.
223#[no_mangle]
224pub unsafe fn render(
225    errmsg: *mut *const c_char,
226    highlights: *const c_char,
227    theme: *const c_char,
228    renderer: *const c_char,
229) -> *const c_char {
230    let highlights = unsafe { string_from_ptr(highlights) };
231    let theme = unsafe { string_from_ptr(theme) };
232    let renderer = unsafe { string_from_ptr(renderer) };
233
234    let highlights = match serde_json::from_str::<_Highlights>(&highlights) {
235        Ok(highlights) => Box::leak(Box::new(highlights)),
236        Err(err) => bail!(errmsg, "highlights are invalid JSON: {err}"),
237    };
238    let highlights: Highlights<'_> = highlights
239        .iter()
240        .map(|line| {
241            line.iter()
242                .map(|(code, hl)| {
243                    (
244                        code.as_str(),
245                        hl.as_ref()
246                            .and_then(|hl| syntastica::theme::THEME_KEYS.iter().find(|k| k == &hl))
247                            .copied(),
248                    )
249                })
250                .collect()
251        })
252        .collect();
253
254    let theme = match serde_json::from_str::<Theme>(&theme) {
255        Ok(Theme::Builtin(theme)) => match syntastica_themes::from_str(&theme) {
256            Some(theme) => theme,
257            None => bail!(errmsg, "unknown builtin theme '{theme}'"),
258        },
259        Ok(Theme::Custom(theme)) => ResolvedTheme::new(theme),
260        Err(err) => bail!(errmsg, "theme is invalid JSON: {err}"),
261    };
262
263    _render(errmsg, &highlights, &renderer, theme)
264}
265
266fn _render(
267    errmsg: *mut *const c_char,
268    highlights: &Highlights<'_>,
269    renderer: &str,
270    theme: ResolvedTheme,
271) -> *const c_char {
272    let out = match renderer.to_lowercase().as_str() {
273        "terminal" => syntastica::render(highlights, &mut TerminalRenderer::new(None), theme),
274        "html" => syntastica::render(highlights, &mut HtmlRenderer, theme),
275        name => match name.strip_prefix("terminal#") {
276            Some(color_hex) => match Color::from_str(color_hex) {
277                Ok(color) => {
278                    syntastica::render(highlights, &mut TerminalRenderer::new(Some(color)), theme)
279                }
280                Err(err) => bail!(
281                    errmsg,
282                    "invalid background color '{color_hex}' for TerminalRenderer: {err}"
283                ),
284            },
285            None => bail!(errmsg, "invalid renderer name '{name}'"),
286        },
287    };
288
289    alloc_string(&out)
290}
291
292/// Serialize a builtin theme to the raw style map.
293///
294/// # Safety
295///
296/// All parameters must be valid pointers.
297#[no_mangle]
298pub unsafe fn get_builtin_theme(errmsg: *mut *const c_char, theme: *const c_char) -> *const c_char {
299    let theme = unsafe { string_from_ptr(theme) };
300
301    let Some(theme) = syntastica_themes::from_str(&theme) else {
302        bail!(errmsg, "unknown builtin theme '{theme}'");
303    };
304
305    match serde_json::to_string(&theme.into_inner()) {
306        Ok(theme) => alloc_string(&theme),
307        Err(err) => bail!(errmsg, "failed serializing theme: {err}"),
308    }
309}