blob: 759a18dbe8fd31d41d5a7a02a05e42482de55d37 [file] [log] [blame]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
use std::collections::HashSet;
use std::ffi::{CStr, CString};
use std::mem::ManuallyDrop;
use std::os::raw::c_char;
use std::sync::{Arc, RwLock};
use lightningcss::css_modules::PatternParseError;
use lightningcss::error::{Error, MinifyErrorKind, ParserError, PrinterError};
use lightningcss::stylesheet::{MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, StyleSheet};
use lightningcss::targets::Browsers;
use parcel_sourcemap::SourceMap;
pub struct StyleSheetWrapper<'i, 'o> {
stylesheet: StyleSheet<'i, 'o>,
source: &'i str,
warnings: Vec<CssError<'i>>,
}
pub struct CssError<'i> {
kind: ErrorKind<'i>,
message: Option<CString>,
}
impl<'i> CssError<'i> {
fn message(&mut self) -> *const c_char {
if let Some(message) = &self.message {
return message.as_ptr();
}
let string: String = match &self.kind {
ErrorKind::ParserError(err) => err.to_string().into(),
ErrorKind::MinifyError(err) => err.to_string().into(),
ErrorKind::PrinterError(err) => err.to_string().into(),
ErrorKind::PatternParseError(err) => err.to_string().into(),
ErrorKind::BrowserslistError(err) => err.to_string().into(),
ErrorKind::SourceMapError(err) => err.to_string().into(),
};
self.message = Some(CString::new(string).unwrap());
self.message.as_ref().unwrap().as_ptr()
}
}
pub enum ErrorKind<'i> {
ParserError(Error<ParserError<'i>>),
MinifyError(Error<MinifyErrorKind>),
PrinterError(PrinterError),
PatternParseError(PatternParseError),
BrowserslistError(browserslist::Error),
SourceMapError(parcel_sourcemap::SourceMapError),
}
macro_rules! impl_from {
($name: ident, $t: ty) => {
impl<'i> From<$t> for CssError<'i> {
fn from(err: $t) -> Self {
CssError {
kind: ErrorKind::$name(err),
message: None,
}
}
}
};
}
impl_from!(ParserError, Error<ParserError<'i>>);
impl_from!(MinifyError, Error<MinifyErrorKind>);
impl_from!(PrinterError, PrinterError);
impl_from!(PatternParseError, PatternParseError);
impl_from!(BrowserslistError, browserslist::Error);
impl_from!(SourceMapError, parcel_sourcemap::SourceMapError);
#[repr(C)]
pub struct ParseOptions {
filename: *const c_char,
nesting: bool,
custom_media: bool,
css_modules: bool,
css_modules_pattern: *const c_char,
css_modules_dashed_idents: bool,
error_recovery: bool,
}
#[repr(C)]
#[derive(Default, PartialEq)]
pub struct Targets {
android: u32,
chrome: u32,
edge: u32,
firefox: u32,
ie: u32,
ios_saf: u32,
opera: u32,
safari: u32,
samsung: u32,
}
impl Into<Browsers> for Targets {
fn into(self) -> Browsers {
macro_rules! browser {
($val: expr) => {
if $val > 0 {
Some($val)
} else {
None
}
};
}
Browsers {
android: browser!(self.android),
chrome: browser!(self.chrome),
edge: browser!(self.edge),
firefox: browser!(self.firefox),
ie: browser!(self.ie),
ios_saf: browser!(self.ios_saf),
opera: browser!(self.opera),
safari: browser!(self.safari),
samsung: browser!(self.samsung),
}
}
}
macro_rules! unwrap {
($result: expr, $error: ident, $ret: expr) => {
match $result {
Ok(v) => v,
Err(err) => unsafe {
*$error = Box::into_raw(Box::new(err.into()));
return $ret;
},
}
};
}
#[no_mangle]
pub extern "C" fn lightningcss_browserslist_to_targets(
query: *const c_char,
targets: *mut Targets,
error: *mut *mut CssError,
) -> bool {
let string = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(query).to_bytes()) };
match Browsers::from_browserslist([string]) {
Ok(Some(browsers)) => {
let targets = unsafe { &mut *targets };
targets.android = browsers.android.unwrap_or_default();
targets.chrome = browsers.chrome.unwrap_or_default();
targets.edge = browsers.edge.unwrap_or_default();
targets.firefox = browsers.firefox.unwrap_or_default();
targets.ie = browsers.ie.unwrap_or_default();
targets.ios_saf = browsers.ios_saf.unwrap_or_default();
targets.opera = browsers.opera.unwrap_or_default();
targets.safari = browsers.safari.unwrap_or_default();
targets.samsung = browsers.samsung.unwrap_or_default();
true
}
Ok(None) => true,
Err(err) => unsafe {
*error = Box::into_raw(Box::new(err.into()));
false
},
}
}
#[repr(C)]
pub struct TransformOptions {
targets: Targets,
unused_symbols: *mut *mut c_char,
unused_symbols_len: usize,
}
impl Into<MinifyOptions> for TransformOptions {
fn into(self) -> MinifyOptions {
let mut unused_symbols = HashSet::new();
let slice = unsafe { std::slice::from_raw_parts(self.unused_symbols, self.unused_symbols_len) };
for symbol in slice {
let string = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(*symbol).to_bytes()).to_owned() };
unused_symbols.insert(string);
}
MinifyOptions {
targets: if self.targets != Targets::default() {
Some(self.targets.into()).into()
} else {
Default::default()
},
unused_symbols,
}
}
}
#[repr(C)]
pub struct ToCssOptions {
minify: bool,
source_map: bool,
input_source_map: *const c_char,
input_source_map_len: usize,
project_root: *const c_char,
targets: Targets,
analyze_dependencies: bool,
pseudo_classes: PseudoClasses,
}
#[derive(PartialEq)]
#[repr(C)]
pub struct PseudoClasses {
hover: *const c_char,
active: *const c_char,
focus: *const c_char,
focus_visible: *const c_char,
focus_within: *const c_char,
}
impl Default for PseudoClasses {
fn default() -> Self {
PseudoClasses {
hover: std::ptr::null(),
active: std::ptr::null(),
focus: std::ptr::null(),
focus_visible: std::ptr::null(),
focus_within: std::ptr::null(),
}
}
}
impl<'a> Into<lightningcss::printer::PseudoClasses<'a>> for PseudoClasses {
fn into(self) -> lightningcss::printer::PseudoClasses<'a> {
macro_rules! pc {
($ptr: expr) => {
if $ptr.is_null() {
None
} else {
Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr($ptr).to_bytes()) })
}
};
}
lightningcss::printer::PseudoClasses {
hover: pc!(self.hover),
active: pc!(self.active),
focus: pc!(self.focus),
focus_visible: pc!(self.focus_visible),
focus_within: pc!(self.focus_within),
}
}
}
#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_parse(
source: *const c_char,
len: usize,
options: ParseOptions,
error: *mut *mut CssError,
) -> *mut StyleSheetWrapper {
let slice = unsafe { std::slice::from_raw_parts(source as *const u8, len) };
let code = unsafe { std::str::from_utf8_unchecked(slice) };
let warnings = Arc::new(RwLock::new(Vec::new()));
let mut flags = ParserFlags::empty();
flags.set(ParserFlags::CUSTOM_MEDIA, options.custom_media);
let opts = ParserOptions {
filename: if options.filename.is_null() {
String::new()
} else {
unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.filename).to_bytes()).to_owned() }
},
flags,
css_modules: if options.css_modules {
let pattern = if !options.css_modules_pattern.is_null() {
let pattern =
unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.css_modules_pattern).to_bytes()) };
unwrap!(
lightningcss::css_modules::Pattern::parse(pattern),
error,
std::ptr::null_mut()
)
} else {
lightningcss::css_modules::Pattern::default()
};
Some(lightningcss::css_modules::Config {
pattern,
dashed_idents: options.css_modules_dashed_idents,
..Default::default()
})
} else {
None
},
error_recovery: options.error_recovery,
source_index: 0,
warnings: Some(warnings.clone()),
};
let stylesheet = unwrap!(StyleSheet::parse(code, opts), error, std::ptr::null_mut());
Box::into_raw(Box::new(StyleSheetWrapper {
stylesheet,
source: code,
warnings: warnings.clone().read().unwrap().iter().map(|w| w.clone().into()).collect(),
}))
}
#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_transform(
stylesheet: *mut StyleSheetWrapper,
options: TransformOptions,
error: *mut *mut CssError,
) -> bool {
let wrapper = unsafe { stylesheet.as_mut() }.unwrap();
unwrap!(wrapper.stylesheet.minify(options.into()), error, false);
true
}
#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_to_css(
stylesheet: *mut StyleSheetWrapper,
options: ToCssOptions,
error: *mut *mut CssError,
) -> ToCssResult {
let wrapper = unsafe { stylesheet.as_mut() }.unwrap();
let mut source_map = if options.source_map {
let mut sm = SourceMap::new("/");
sm.add_source(&wrapper.stylesheet.sources[0]);
unwrap!(sm.set_source_content(0, wrapper.source), error, ToCssResult::default());
Some(sm)
} else {
None
};
let opts = PrinterOptions {
minify: options.minify,
project_root: if options.project_root.is_null() {
None
} else {
Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.project_root).to_bytes()) })
},
source_map: source_map.as_mut(),
targets: if options.targets != Targets::default() {
Some(options.targets.into()).into()
} else {
Default::default()
},
analyze_dependencies: if options.analyze_dependencies {
Some(Default::default())
} else {
None
},
pseudo_classes: if options.pseudo_classes != PseudoClasses::default() {
Some(options.pseudo_classes.into())
} else {
None
},
};
let res = unwrap!(wrapper.stylesheet.to_css(opts), error, ToCssResult::default());
let map = if let Some(mut source_map) = source_map {
if !options.input_source_map.is_null() {
let slice =
unsafe { std::slice::from_raw_parts(options.input_source_map as *const u8, options.input_source_map_len) };
let input_source_map = unsafe { std::str::from_utf8_unchecked(slice) };
let mut sm = unwrap!(
SourceMap::from_json("/", input_source_map),
error,
ToCssResult::default()
);
unwrap!(source_map.extends(&mut sm), error, ToCssResult::default());
}
unwrap!(source_map.to_json(None), error, ToCssResult::default()).into()
} else {
RawString::default()
};
let (exports, exports_len) = if let Some(exports) = res.exports {
let exports: Vec<CssModuleExport> = exports
.into_iter()
.map(|(k, v)| {
let composes_len = v.composes.len();
let composes = if !v.composes.is_empty() {
let composes: Vec<CssModuleReference> = v.composes.into_iter().map(|composes| composes.into()).collect();
ManuallyDrop::new(composes).as_mut_ptr()
} else {
std::ptr::null_mut()
};
CssModuleExport {
exported: k.into(),
local: v.name.into(),
is_referenced: v.is_referenced,
composes,
composes_len,
}
})
.collect();
let mut exports = ManuallyDrop::new(exports);
(exports.as_mut_ptr(), exports.len())
} else {
(std::ptr::null_mut(), 0)
};
let (references, references_len) = if let Some(references) = res.references {
let references: Vec<CssModulePlaceholder> = references
.into_iter()
.map(|(k, v)| CssModulePlaceholder {
placeholder: k.into(),
reference: v.into(),
})
.collect();
let mut references = ManuallyDrop::new(references);
(references.as_mut_ptr(), references.len())
} else {
(std::ptr::null_mut(), 0)
};
ToCssResult {
code: res.code.into(),
map,
exports,
exports_len,
references,
references_len,
}
}
#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_free(stylesheet: *mut StyleSheetWrapper) {
if !stylesheet.is_null() {
drop(unsafe { Box::from_raw(stylesheet) })
}
}
#[repr(C)]
pub struct ToCssResult {
code: RawString,
map: RawString,
exports: *mut CssModuleExport,
exports_len: usize,
references: *mut CssModulePlaceholder,
references_len: usize,
}
impl Default for ToCssResult {
fn default() -> Self {
ToCssResult {
code: RawString::default(),
map: RawString::default(),
exports: std::ptr::null_mut(),
exports_len: 0,
references: std::ptr::null_mut(),
references_len: 0,
}
}
}
impl Drop for ToCssResult {
fn drop(&mut self) {
if !self.exports.is_null() {
let exports = unsafe { Vec::from_raw_parts(self.exports, self.exports_len, self.exports_len) };
drop(exports);
self.exports = std::ptr::null_mut();
}
if !self.references.is_null() {
let references = unsafe { Vec::from_raw_parts(self.references, self.references_len, self.references_len) };
drop(references);
self.references = std::ptr::null_mut();
}
}
}
#[no_mangle]
pub extern "C" fn lightningcss_to_css_result_free(result: ToCssResult) {
drop(result)
}
#[repr(C)]
pub struct CssModuleExport {
exported: RawString,
local: RawString,
is_referenced: bool,
composes: *mut CssModuleReference,
composes_len: usize,
}
impl Drop for CssModuleExport {
fn drop(&mut self) {
if !self.composes.is_null() {
let composes = unsafe { Vec::from_raw_parts(self.composes, self.composes_len, self.composes_len) };
drop(composes);
self.composes = std::ptr::null_mut();
}
}
}
#[repr(C)]
pub enum CssModuleReference {
/// A local reference.
Local {
/// The local (compiled) name for the reference.
name: RawString,
},
/// A global reference.
Global {
/// The referenced global name.
name: RawString,
},
/// A reference to an export in a different file.
Dependency {
/// The name to reference within the dependency.
name: RawString,
/// The dependency specifier for the referenced file.
specifier: RawString,
},
}
impl From<lightningcss::css_modules::CssModuleReference> for CssModuleReference {
fn from(reference: lightningcss::css_modules::CssModuleReference) -> Self {
use lightningcss::css_modules::CssModuleReference::*;
match reference {
Local { name } => CssModuleReference::Local { name: name.into() },
Global { name } => CssModuleReference::Global { name: name.into() },
Dependency { name, specifier } => CssModuleReference::Dependency {
name: name.into(),
specifier: specifier.into(),
},
}
}
}
#[repr(C)]
pub struct CssModulePlaceholder {
placeholder: RawString,
reference: CssModuleReference,
}
#[repr(C)]
pub struct RawString {
text: *mut c_char,
len: usize,
}
impl Default for RawString {
fn default() -> Self {
RawString {
text: std::ptr::null_mut(),
len: 0,
}
}
}
impl From<String> for RawString {
fn from(string: String) -> RawString {
RawString {
len: string.len(),
text: Box::into_raw(string.into_boxed_str()) as *mut c_char,
}
}
}
impl Drop for RawString {
fn drop(&mut self) {
if self.text.is_null() {
return;
}
drop(unsafe { Box::from_raw(self.text) });
self.text = std::ptr::null_mut();
}
}
#[no_mangle]
pub extern "C" fn lightningcss_error_message(error: *mut CssError) -> *const c_char {
match unsafe { error.as_mut() } {
Some(err) => err.message(),
None => std::ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn lightningcss_error_free(error: *mut CssError) {
if !error.is_null() {
drop(unsafe { Box::from_raw(error) })
}
}
#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_get_warning_count<'i>(
stylesheet: *mut StyleSheetWrapper<'i, '_>,
) -> usize {
match unsafe { stylesheet.as_mut() } {
Some(s) => s.warnings.len(),
None => 0,
}
}
#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_get_warning<'i>(
stylesheet: *mut StyleSheetWrapper<'i, '_>,
index: usize,
) -> *const c_char {
let stylesheet = match unsafe { stylesheet.as_mut() } {
Some(s) => s,
None => return std::ptr::null(),
};
match stylesheet.warnings.get_mut(index) {
Some(w) => w.message(),
None => std::ptr::null(),
}
}