1use colorfulcode::{ansi, highlight, html, Kind, Language, Theme};
18use std::ffi::{c_char, CStr, CString};
19use std::panic::{catch_unwind, AssertUnwindSafe};
20use std::ptr;
21use std::sync::OnceLock;
22
23pub const CC_UTF16_OFFSETS: u32 = 1 << 0;
25pub const CC_HTML_WRAP: u32 = 1 << 1;
27pub const CC_HTML_LINES: u32 = 1 << 2;
29pub const CC_ANSI_256: u32 = 1 << 3;
31
32#[repr(C)]
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct CcToken {
36 pub start: u32,
37 pub end: u32,
38 pub kind: u8,
40}
41
42#[repr(C)]
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct CcStyle {
46 pub color: u32,
48 pub bold: u8,
49 pub italic: u8,
50 pub underline: u8,
51}
52
53pub struct CcTokens(Vec<CcToken>);
55
56struct CStrings(OnceLock<Vec<CString>>);
59
60impl CStrings {
61 const fn new() -> CStrings {
62 CStrings(OnceLock::new())
63 }
64
65 fn get(&self, index: Option<usize>, init: impl FnOnce() -> Vec<&'static str>) -> *const c_char {
66 let all = self.0.get_or_init(|| init().into_iter().map(|s| CString::new(s).unwrap_or_default()).collect());
67 index.and_then(|i| all.get(i)).map_or(ptr::null(), |s| s.as_ptr())
68 }
69}
70
71fn guard<T>(fallback: T, f: impl FnOnce() -> T) -> T {
73 catch_unwind(AssertUnwindSafe(f)).unwrap_or(fallback)
74}
75
76fn index(i: i32) -> Option<usize> {
77 usize::try_from(i).ok()
78}
79
80fn to_index(i: Option<usize>) -> i32 {
81 i.and_then(|i| i32::try_from(i).ok()).unwrap_or(-1)
82}
83
84fn language_at(i: i32) -> Language {
85 index(i).and_then(|i| Language::all().get(i)).copied().unwrap_or_else(Language::plain)
86}
87
88fn language_index(language: Option<Language>) -> i32 {
89 to_index(language.and_then(|l| Language::all().iter().position(|x| *x == l)))
90}
91
92fn theme_at(i: i32) -> Option<&'static Theme> {
93 index(i).and_then(|i| Theme::all().get(i))
94}
95
96fn theme_or_default(i: i32) -> &'static Theme {
97 theme_at(i).unwrap_or(&Theme::all()[0])
98}
99
100unsafe fn str_arg<'a>(s: *const c_char) -> Option<&'a str> {
102 if s.is_null() {
103 return None;
104 }
105 unsafe { CStr::from_ptr(s) }.to_str().ok()
106}
107
108unsafe fn source<'a>(code: *const c_char, len: usize) -> Option<&'a str> {
111 if code.is_null() {
112 return (len == 0).then_some("");
113 }
114 if len > u32::MAX as usize {
115 return None;
116 }
117 std::str::from_utf8(unsafe { std::slice::from_raw_parts(code.cast::<u8>(), len) }).ok()
118}
119
120fn into_c_string(s: String) -> *mut c_char {
121 CString::new(s).map_or(ptr::null_mut(), CString::into_raw)
122}
123
124#[no_mangle]
126pub extern "C" fn cc_version() -> *const c_char {
127 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
128}
129
130#[no_mangle]
132pub extern "C" fn cc_language_count() -> i32 {
133 to_index(Some(Language::all().len()))
134}
135
136#[no_mangle]
142pub unsafe extern "C" fn cc_language_find(name: *const c_char) -> i32 {
143 language_index(unsafe { str_arg(name) }.and_then(Language::find))
144}
145
146#[no_mangle]
152pub unsafe extern "C" fn cc_language_from_path(path: *const c_char) -> i32 {
153 language_index(unsafe { str_arg(path) }.and_then(Language::from_path))
154}
155
156#[no_mangle]
162pub unsafe extern "C" fn cc_language_from_content(code: *const c_char, len: usize) -> i32 {
163 language_index(unsafe { source(code, len) }.and_then(Language::from_content))
164}
165
166#[no_mangle]
174pub unsafe extern "C" fn cc_language_detect(code: *const c_char, len: usize) -> i32 {
175 let Some(code) = (unsafe { source(code, len) }) else {
176 return -1;
177 };
178 guard(-1, || language_index(Language::detect(code)))
179}
180
181#[no_mangle]
183pub extern "C" fn cc_language_id(language: i32) -> *const c_char {
184 static IDS: CStrings = CStrings::new();
185 IDS.get(index(language), || Language::all().iter().map(|l| l.id()).collect())
186}
187
188#[no_mangle]
190pub extern "C" fn cc_language_name(language: i32) -> *const c_char {
191 static NAMES: CStrings = CStrings::new();
192 NAMES.get(index(language), || Language::all().iter().map(|l| l.name()).collect())
193}
194
195#[no_mangle]
202pub unsafe extern "C" fn cc_highlight(code: *const c_char, len: usize, language: i32, flags: u32) -> *mut CcTokens {
203 let Some(code) = (unsafe { source(code, len) }) else {
204 return ptr::null_mut();
205 };
206 guard(ptr::null_mut(), || {
207 let highlighted = highlight(code, language_at(language));
208 let tokens = if flags & CC_UTF16_OFFSETS != 0 { highlighted.utf16_tokens() } else { highlighted.into_tokens() };
209 let tokens = tokens.iter().map(|t| CcToken { start: t.start, end: t.end, kind: t.kind as u8 }).collect();
210 Box::into_raw(Box::new(CcTokens(tokens)))
211 })
212}
213
214#[no_mangle]
220pub unsafe extern "C" fn cc_tokens_len(tokens: *const CcTokens) -> usize {
221 unsafe { tokens.as_ref() }.map_or(0, |t| t.0.len())
222}
223
224#[no_mangle]
230pub unsafe extern "C" fn cc_tokens_data(tokens: *const CcTokens) -> *const CcToken {
231 unsafe { tokens.as_ref() }.map_or(ptr::null(), |t| t.0.as_ptr())
232}
233
234#[no_mangle]
240pub unsafe extern "C" fn cc_tokens_free(tokens: *mut CcTokens) {
241 if !tokens.is_null() {
242 drop(unsafe { Box::from_raw(tokens) });
243 }
244}
245
246#[no_mangle]
248pub extern "C" fn cc_kind_name(kind: u8) -> *const c_char {
249 static NAMES: CStrings = CStrings::new();
250 NAMES.get(Some(kind.into()), || Kind::ALL.iter().map(|k| k.name()).collect())
251}
252
253#[no_mangle]
255pub extern "C" fn cc_theme_count() -> i32 {
256 to_index(Some(Theme::all().len()))
257}
258
259#[no_mangle]
265pub unsafe extern "C" fn cc_theme_find(name: *const c_char) -> i32 {
266 let theme = unsafe { str_arg(name) }.and_then(Theme::find);
267 to_index(theme.and_then(|t| Theme::all().iter().position(|x| ptr::eq(x, t))))
268}
269
270#[no_mangle]
272pub extern "C" fn cc_theme_name(theme: i32) -> *const c_char {
273 static NAMES: CStrings = CStrings::new();
274 NAMES.get(index(theme), || Theme::all().iter().map(|t| t.name).collect())
275}
276
277#[no_mangle]
279pub extern "C" fn cc_theme_is_dark(theme: i32) -> u8 {
280 theme_or_default(theme).dark.into()
281}
282
283#[no_mangle]
285pub extern "C" fn cc_theme_background(theme: i32) -> u32 {
286 theme_or_default(theme).background.to_u32()
287}
288
289#[no_mangle]
291pub extern "C" fn cc_theme_foreground(theme: i32) -> u32 {
292 theme_or_default(theme).foreground.to_u32()
293}
294
295#[no_mangle]
298pub extern "C" fn cc_theme_style(theme: i32, kind: u8) -> CcStyle {
299 let style = theme_or_default(theme).style(Kind::from_u8(kind).unwrap_or(Kind::Text));
300 CcStyle {
301 color: style.color.to_u32(),
302 bold: style.bold.into(),
303 italic: style.italic.into(),
304 underline: style.underline.into(),
305 }
306}
307
308#[no_mangle]
315pub unsafe extern "C" fn cc_theme_css(theme: i32, selector: *const c_char) -> *mut c_char {
316 let selector = if selector.is_null() { Some(".cc") } else { unsafe { str_arg(selector) } };
317 let Some(selector) = selector else {
318 return ptr::null_mut();
319 };
320 guard(ptr::null_mut(), || into_c_string(theme_or_default(theme).to_css(selector)))
321}
322
323#[no_mangle]
331pub unsafe extern "C" fn cc_render_html(
332 code: *const c_char,
333 len: usize,
334 language: i32,
335 theme: i32,
336 flags: u32,
337) -> *mut c_char {
338 let Some(code) = (unsafe { source(code, len) }) else {
339 return ptr::null_mut();
340 };
341 guard(ptr::null_mut(), || {
342 let options = html::Options {
343 theme: theme_at(theme),
344 wrap: flags & CC_HTML_WRAP != 0,
345 lines: flags & CC_HTML_LINES != 0,
346 ..Default::default()
347 };
348 into_c_string(highlight(code, language_at(language)).to_html(&options))
349 })
350}
351
352#[no_mangle]
360pub unsafe extern "C" fn cc_render_ansi(
361 code: *const c_char,
362 len: usize,
363 language: i32,
364 theme: i32,
365 flags: u32,
366) -> *mut c_char {
367 let Some(code) = (unsafe { source(code, len) }) else {
368 return ptr::null_mut();
369 };
370 guard(ptr::null_mut(), || {
371 let mode = if flags & CC_ANSI_256 != 0 { ansi::ColorMode::Ansi256 } else { ansi::ColorMode::TrueColor };
372 into_c_string(highlight(code, language_at(language)).to_ansi(theme_or_default(theme), mode))
373 })
374}
375
376#[no_mangle]
382pub unsafe extern "C" fn cc_string_free(s: *mut c_char) {
383 if !s.is_null() {
384 drop(unsafe { CString::from_raw(s) });
385 }
386}