flams_utils/
lib.rs

1//#![feature(ptr_as_ref_unchecked)]
2#![cfg_attr(docsrs, feature(doc_auto_cfg))]
3
4pub mod binary;
5#[cfg(feature = "async")]
6pub mod change_listener;
7pub mod escaping;
8pub mod gc;
9pub mod globals;
10pub mod id_counters;
11mod inner_arc;
12pub mod logs;
13pub mod parsing;
14pub mod regex;
15pub mod settings;
16pub mod sourcerefs;
17pub mod time;
18mod treelike;
19pub mod vecmap;
20//pub mod file_id;
21
22pub use parking_lot;
23pub use triomphe;
24
25pub mod prelude {
26    pub use super::vecmap::{VecMap, VecSet};
27    pub type HMap<K, V> = rustc_hash::FxHashMap<K, V>;
28    pub type HSet<V> = rustc_hash::FxHashSet<V>;
29    pub use crate::inner_arc::InnerArc;
30    pub use crate::treelike::*;
31}
32
33#[cfg(target_family = "wasm")]
34type Str = String;
35#[cfg(not(target_family = "wasm"))]
36type Str = Box<str>;
37
38pub fn hashstr<A: std::hash::Hash>(prefix: &str, a: &A) -> String {
39    use std::hash::BuildHasher;
40    let h = rustc_hash::FxBuildHasher.hash_one(a);
41    format!("{prefix}{h:02x}")
42}
43
44#[cfg(feature = "tokio")]
45pub fn background<F: FnOnce() + Send + 'static>(f: F) {
46    let span = tracing::Span::current();
47    tokio::task::spawn_blocking(move || span.in_scope(f));
48}
49
50pub fn in_span<F: FnOnce() -> R, R>(f: F) -> impl FnOnce() -> R {
51    let span = tracing::Span::current();
52    move || {
53        let _span = span.enter();
54        f()
55    }
56}
57
58#[cfg(feature = "serde")]
59pub trait Hexable: Sized {
60    /// #### Errors
61    fn as_hex(&self) -> eyre::Result<String>;
62    /// #### Errors
63    fn from_hex(s: &str) -> eyre::Result<Self>;
64}
65#[cfg(feature = "serde")]
66impl<T: Sized + serde::Serialize + for<'de> serde::Deserialize<'de>> Hexable for T {
67    fn as_hex(&self) -> eyre::Result<String> {
68        use std::fmt::Write;
69        let bc = bincode::serde::encode_to_vec(self, bincode::config::standard())?;
70        let mut ret = String::with_capacity(bc.len() * 2);
71        for b in bc {
72            write!(ret, "{b:02X}")?;
73        }
74        Ok(ret)
75    }
76    fn from_hex(s: &str) -> eyre::Result<Self> {
77        let bytes: Result<Vec<_>, _> = if s.len() % 2 == 0 {
78            (0..s.len())
79                .step_by(2)
80                .filter_map(|i| s.get(i..i + 2))
81                .map(|sub| u8::from_str_radix(sub, 16))
82                .collect()
83        } else {
84            return Err(eyre::eyre!("Incompatible string length"));
85        };
86        bincode::serde::decode_from_slice(&bytes?, bincode::config::standard())
87            .map(|(r, _)| r)
88            .map_err(Into::into)
89    }
90}
91
92pub mod fs {
93    use std::path::Path;
94
95    use eyre::Context;
96
97    /// #### Errors
98    pub fn copy_dir_all(src: &Path, dst: &Path) -> eyre::Result<()> {
99        std::fs::create_dir_all(dst).wrap_err_with(|| format!("Error creating {}", dst.display()))?;
100        for entry in std::fs::read_dir(src).wrap_err_with(|| format!("Error reading {}", src.display()))? {
101            let entry = entry.wrap_err_with(|| format!("Error getting file entry for {}", src.display()))?;
102            let ty = entry
103                .file_type()
104                .wrap_err_with(|| format!("Error determining file type of {}", entry.path().display()))?;
105            let target = dst.join(entry.file_name());
106            if ty.is_dir() {
107                copy_dir_all(&entry.path(), &target)?;
108            } else {
109                let md = entry
110                    .metadata()
111                    .wrap_err_with(|| format!("Error obtaining metatada for {}", entry.path().display()))?;
112                std::fs::copy(entry.path(), &target).wrap_err_with(|| {
113                    format!("Error copying {} to {}", entry.path().display(), target.display())
114                })?;
115                let mtime = filetime::FileTime::from_last_modification_time(&md);
116                filetime::set_file_mtime(&target, mtime)
117                    .wrap_err_with(|| format!("Error setting file modification time for {}", target.display()))?;
118            }
119        }
120        Ok(())
121    }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
126#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128pub enum CSS {
129    Link(#[cfg_attr(feature = "wasm", tsify(type = "string"))] Str),
130    Inline(#[cfg_attr(feature = "wasm", tsify(type = "string"))] Str),
131    Class {
132        #[cfg_attr(feature = "wasm", tsify(type = "string"))]
133        name: Str,
134        #[cfg_attr(feature = "wasm", tsify(type = "string"))]
135        css: Str,
136    },
137}
138impl PartialOrd for CSS {
139    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
140        Some(self.cmp(other))
141    }
142}
143impl Ord for CSS {
144    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
145        fn classnum(s: &str) -> u8 {
146            match s {
147                s if s.starts_with("ftml-subproblem") => 1,
148                s if s.starts_with("ftml-problem") => 2,
149                s if s.starts_with("ftml-example") => 3,
150                s if s.starts_with("ftml-definition") => 4,
151                s if s.starts_with("ftml-paragraph") => 5,
152                "ftml-subsubsection" => 6,
153                "ftml-subsection" => 7,
154                "ftml-section" => 8,
155                "ftml-chapter" => 9,
156                "ftml-part" => 10,
157                _ => 0,
158            }
159        }
160        use std::cmp::Ordering;
161        match (self, other) {
162            (Self::Link(l1), Self::Link(l2)) | (Self::Inline(l1), Self::Inline(l2)) => l1.cmp(l2),
163            (Self::Link(_), Self::Inline(_))
164            | (Self::Link(_) | Self::Inline(_), Self::Class { .. }) => Ordering::Less,
165            (Self::Inline(_), Self::Link(_))
166            | (Self::Class { .. }, Self::Inline(_) | Self::Link(_)) => Ordering::Greater,
167            (Self::Class { name: n1, css: c1 }, Self::Class { name: n2, css: c2 }) => {
168                (classnum(n1), n1, c1).cmp(&(classnum(n2), n2, c2))
169            }
170        }
171    }
172}
173impl CSS {
174
175    pub fn merge(v:Vec<Self>) -> Vec<Self> {
176        use lightningcss::traits::ToCss;
177        use lightningcss::{
178            printer::PrinterOptions,
179            rules::{CssRule,CssRuleList},
180            selector::Component,
181            stylesheet::{ParserOptions, StyleSheet,MinifyOptions},
182        };
183        
184        let mut links = Vec::new();
185        let mut strings = Vec::new();
186        for c in v {
187            match c {
188                Self::Link(_) => links.push(c),
189                Self::Inline(css) | Self::Class{ css,..} => strings.push(css)
190            }
191        }
192        
193        let mut sheet = StyleSheet::new(Vec::new(), CssRuleList(Vec::new()), ParserOptions::default());
194        let mut inlines = smallvec::SmallVec::<_,2>::new();
195        for (i,s) in strings.iter().enumerate() {
196            if let Ok(rs) = StyleSheet::parse(s,ParserOptions::default()) {
197                sheet.rules.0.extend(rs.rules.0.into_iter());
198            } else {
199                tracing::warn!("Not class-able: {s}");
200            }
201        }
202        let _ = sheet.minify(MinifyOptions::default());
203        
204        let mut classes = Vec::new();
205        for rule in std::mem::take(&mut sheet.rules.0)  {
206            match rule {
207                CssRule::Style(style) => {
208                    if style.vendor_prefix.is_empty()
209                        && style.selectors.0.len() == 1
210                        && style.selectors.0[0].len() == 1
211                        && matches!(
212                            style.selectors.0[0].iter().next(),
213                            Some(Component::Class(_))
214                        )
215                    {
216                        let Some(Component::Class(class_name)) = style.selectors.0[0].iter().next()
217                        else {
218                            impossible!()
219                        };
220                        if let Ok(s) = style.to_css_string(PrinterOptions::default()) {
221                            classes.push(Self::Class {
222                                name: class_name.to_string().into(),
223                                css: s.into(),
224                            });
225                        } else {
226                            tracing::warn!("Illegal CSS: {style:?}");
227                        }
228                    } else {
229                        if let Ok(s) = style.to_css_string(PrinterOptions::default()) {
230                            tracing::warn!("Not class-able: {s}");
231                            links.push(Self::Inline(s.into()));
232                        } else {
233                            tracing::warn!("Illegal CSS: {style:?}");
234                        }
235                    }
236                }
237                rule => {
238                        if let Ok(s) = rule.to_css_string(PrinterOptions::default()) {
239                            tracing::warn!("Not class-able: {s}");
240                            links.push(Self::Inline(s.into()));
241                        } else {
242                            tracing::warn!("Illegal CSS: {rule:?}");
243                        }
244                    }
245            }
246        }
247        drop(sheet);
248        
249        links.extend(inlines.into_iter().map(|i| Self::Inline(strings.remove(i))));
250        links.extend(classes);
251        links
252    }
253
254    #[must_use]
255    pub fn split(css: &str) -> Vec<Self> {
256        use lightningcss::traits::ToCss;
257        use lightningcss::{
258            printer::PrinterOptions,
259            rules::CssRule,
260            selector::Component,
261            stylesheet::{ParserOptions, StyleSheet},
262        };
263        let Ok(ruleset) = StyleSheet::parse(css, ParserOptions::default()) else {
264            tracing::warn!("Not class-able: {css}");
265            return vec![Self::Inline(css.to_string().into())];
266        };
267        if ruleset.sources.iter().any(|s| !s.is_empty()) {
268            tracing::warn!("Not class-able: {css}");
269            return vec![Self::Inline(css.to_string().into())];
270        }
271        ruleset
272            .rules
273            .0
274            .into_iter()
275            .filter_map(|rule| match rule {
276                CssRule::Style(style) => {
277                    if style.vendor_prefix.is_empty()
278                        && style.selectors.0.len() == 1
279                        && style.selectors.0[0].len() == 1
280                        && matches!(
281                            style.selectors.0[0].iter().next(),
282                            Some(Component::Class(_))
283                        )
284                    {
285                        let Some(Component::Class(class_name)) = style.selectors.0[0].iter().next()
286                        else {
287                            impossible!()
288                        };
289                        style
290                            .to_css_string(PrinterOptions::default())
291                            .ok()
292                            .map(|s| Self::Class {
293                                name: class_name.to_string().into(),
294                                css: s.into(),
295                            })
296                    } else {
297                        style
298                            .to_css_string(PrinterOptions::default())
299                            .ok()
300                            .map(|s| {
301                                tracing::warn!("Not class-able: {s}");
302                                Self::Inline(s.into())
303                            })
304                    }
305                }
306                o => o.to_css_string(PrinterOptions::default()).ok().map(|s| {
307                    tracing::warn!("Not class-able: {s}");
308                    Self::Inline(s.into())
309                }),
310            })
311            .collect()
312    }
313}
314
315#[macro_export]
316macro_rules! impossible {
317    () => {{
318        #[cfg(debug_assertions)]
319        {
320            unreachable!()
321        }
322        #[cfg(not(debug_assertions))]
323        {
324            unsafe { std::hint::unreachable_unchecked() }
325        }
326    }};
327    ($s:literal) => {
328        #[cfg(debug_assertions)]
329        {
330            panic!($s)
331        }
332        #[cfg(not(debug_assertions))]
333        {
334            unsafe { std::hint::unreachable_unchecked() }
335        }
336    };
337    (?) => {
338        unreachable!()
339    };
340    (? $s:literal) => {{
341        panic!($s)
342    }};
343}
344
345#[macro_export]
346macro_rules! unwrap {
347    ($e: expr) => { $e.unwrap_or_else(|| {$crate::impossible!();}) };
348    (? $e: expr) => { $e.unwrap_or_else(|| {$crate::impossible!(?);}) };
349    ($e: expr;$l:literal) => { $e.unwrap_or_else(|| {$crate::impossible!($l);}) };
350    (? $e: expr;$l:literal) => { $e.unwrap_or_else(|| {$crate::impossible!(? $l);}) };
351}
352
353#[cfg(feature = "serde")]
354pub trait CondSerialize: serde::Serialize {}
355#[cfg(feature = "serde")]
356impl<T: serde::Serialize> CondSerialize for T {}
357
358#[cfg(not(feature = "serde"))]
359pub trait CondSerialize {}
360#[cfg(not(feature = "serde"))]
361impl<T> CondSerialize for T {}
362
363#[allow(clippy::unwrap_used)]
364#[allow(clippy::cognitive_complexity)]
365#[allow(clippy::similar_names)]
366#[test]
367fn css_things() {
368    use lightningcss::traits::ToCss;
369    use lightningcss::{
370        printer::PrinterOptions,
371        rules::CssRule,
372        selector::Component,
373        stylesheet::{MinifyOptions, ParserOptions, StyleSheet},
374    };
375    tracing_subscriber::fmt().init();
376    let css = include_str!("../../../resources/assets/rustex.css");
377    let rules = StyleSheet::parse(css, ParserOptions::default()).unwrap();
378    let roundtrip = rules.to_css(PrinterOptions::default()).unwrap();
379    tracing::info!("{}", roundtrip.code);
380    let test = "
381        .ftml-paragraph {
382            > .ftml-title {
383                font-weight: bold;
384            }
385            margin: 0;
386        }
387    ";
388    let mut ruleset = StyleSheet::parse(test, ParserOptions::default()).unwrap();
389    ruleset.minify(MinifyOptions::default()).unwrap();
390    assert!(ruleset.sources.iter().all(String::is_empty));
391    tracing::info!("Result: {ruleset:#?}");
392    for rule in ruleset.rules.0 {
393        match rule {
394            CssRule::Style(style) => {
395                assert!(style.vendor_prefix.is_empty());
396                assert!(style.selectors.0.len() == 1);
397                assert!(style.selectors.0[0].len() == 1);
398                tracing::info!(
399                    "Here: {}",
400                    style.to_css_string(PrinterOptions::default()).unwrap()
401                );
402                let sel = style.selectors.0[0].iter().next().unwrap();
403                assert!(matches!(sel, Component::Class(_)));
404                let Component::Class(cls) = sel else {
405                    impossible!()
406                };
407                let cls_str = &**cls;
408                tracing::info!("Class: {cls_str}");
409            }
410            o => panic!("Unexpected rule: {o:#?}"),
411        }
412    }
413}
414
415pub trait PathExt {
416    const PATH_SEPARATOR: char;
417    fn as_slash_str(&self) -> String;
418    fn same_fs_as<P:AsRef<std::path::Path>>(&self,other:&P) -> bool;
419    fn rename_safe<P:AsRef<std::path::Path>>(&self,target:&P) -> eyre::Result<()>;
420}
421impl<T:AsRef<std::path::Path>> PathExt for T {
422    
423    #[cfg(target_os = "windows")]
424    const PATH_SEPARATOR: char = '\\';
425    #[cfg(not(target_os = "windows"))]
426    const PATH_SEPARATOR: char = '/';
427    fn as_slash_str(&self) -> String {
428        if cfg!(windows) {
429            unwrap!(self.as_ref().as_os_str().to_str()).replace('\\',"/")
430        } else {
431            unwrap!(self.as_ref().as_os_str().to_str()).to_string()
432        }
433    }
434    #[cfg(target_os = "windows")]
435    fn same_fs_as<P:AsRef<std::path::Path>>(&self,other:&P) -> bool {
436        let Some(p1) = self.as_ref().components().next().and_then(|c| c.as_os_str().to_str()) else {
437            return false;
438        };
439        let Some(p2) = other.as_ref().components().next().and_then(|c| c.as_os_str().to_str()) else {
440            return false;
441        };
442        p1 == p2
443    }
444    #[cfg(target_arch="wasm32")]
445    fn same_fs_as<P:AsRef<std::path::Path>>(&self,other:&P) -> bool {
446        impossible!()
447    }
448
449    #[cfg(not(any(target_os = "windows",target_arch="wasm32")))]
450    fn same_fs_as<P:AsRef<std::path::Path>>(&self,other:&P) -> bool {
451        use std::os::unix::fs::MetadataExt;
452        fn existent_parent(p: &std::path::Path) -> &std::path::Path {
453            if p.exists() {
454                return p;
455            }
456            existent_parent(p.parent().unwrap_or_else(|| unreachable!()))
457        }
458        let p1 = existent_parent(self.as_ref());
459        let p2 = existent_parent(other.as_ref());
460        let md1 = p1.metadata().unwrap_or_else(|_| unreachable!());
461        let md2 = p2.metadata().unwrap_or_else(|_| unreachable!());
462        md1.dev() == md2.dev()
463    }
464    fn rename_safe<P:AsRef<std::path::Path>>(&self,target:&P) -> eyre::Result<()> {
465        Ok(if self.same_fs_as(target) {
466            std::fs::rename(self.as_ref(), target.as_ref())?
467        } else {
468            crate::fs::copy_dir_all(self.as_ref(), target.as_ref())?
469        })
470    }
471}