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#[no_mangle]
141pub unsafe fn add_language(info: *mut LangInfo) {
142 LANGUAGES.add_lang(unsafe { &*info });
143}
144
145#[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#[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#[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#[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}