flams_utils/lib.rs
1//#![feature(ptr_as_ref_unchecked)]
2#![cfg_attr(docsrs, feature(doc_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;
14//pub mod regex;
15pub mod settings;
16pub mod sourcerefs;
17//pub 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().is_multiple_of(2) {
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)
100 .wrap_err_with(|| format!("Error creating {}", dst.display()))?;
101 for entry in
102 std::fs::read_dir(src).wrap_err_with(|| format!("Error reading {}", src.display()))?
103 {
104 let entry = entry
105 .wrap_err_with(|| format!("Error getting file entry for {}", src.display()))?;
106 let ty = entry.file_type().wrap_err_with(|| {
107 format!("Error determining file type of {}", entry.path().display())
108 })?;
109 let target = dst.join(entry.file_name());
110 if ty.is_dir() {
111 copy_dir_all(&entry.path(), &target)?;
112 } else {
113 let md = entry.metadata().wrap_err_with(|| {
114 format!("Error obtaining metatada for {}", entry.path().display())
115 })?;
116 std::fs::copy(entry.path(), &target).wrap_err_with(|| {
117 format!(
118 "Error copying {} to {}",
119 entry.path().display(),
120 target.display()
121 )
122 })?;
123 let mtime = filetime::FileTime::from_last_modification_time(&md);
124 filetime::set_file_mtime(&target, mtime).wrap_err_with(|| {
125 format!(
126 "Error setting file modification time for {}",
127 target.display()
128 )
129 })?;
130 }
131 }
132 Ok(())
133 }
134}
135/*
136#[derive(Debug, Clone, PartialEq, Eq)]
137#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
138#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
139#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
140pub enum CSS {
141 Link(#[cfg_attr(feature = "wasm", tsify(type = "string"))] Str),
142 Inline(#[cfg_attr(feature = "wasm", tsify(type = "string"))] Str),
143 Class {
144 #[cfg_attr(feature = "wasm", tsify(type = "string"))]
145 name: Str,
146 #[cfg_attr(feature = "wasm", tsify(type = "string"))]
147 css: Str,
148 },
149}
150impl PartialOrd for CSS {
151 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
152 Some(self.cmp(other))
153 }
154}
155impl Ord for CSS {
156 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
157 fn classnum(s: &str) -> u8 {
158 match s {
159 s if s.starts_with("ftml-subproblem") => 1,
160 s if s.starts_with("ftml-problem") => 2,
161 s if s.starts_with("ftml-example") => 3,
162 s if s.starts_with("ftml-definition") => 4,
163 s if s.starts_with("ftml-paragraph") => 5,
164 "ftml-subsubsection" => 6,
165 "ftml-subsection" => 7,
166 "ftml-section" => 8,
167 "ftml-chapter" => 9,
168 "ftml-part" => 10,
169 _ => 0,
170 }
171 }
172 use std::cmp::Ordering;
173 match (self, other) {
174 (Self::Link(l1), Self::Link(l2)) | (Self::Inline(l1), Self::Inline(l2)) => l1.cmp(l2),
175 (Self::Link(_), Self::Inline(_))
176 | (Self::Link(_) | Self::Inline(_), Self::Class { .. }) => Ordering::Less,
177 (Self::Inline(_), Self::Link(_))
178 | (Self::Class { .. }, Self::Inline(_) | Self::Link(_)) => Ordering::Greater,
179 (Self::Class { name: n1, css: c1 }, Self::Class { name: n2, css: c2 }) => {
180 (classnum(n1), n1, c1).cmp(&(classnum(n2), n2, c2))
181 }
182 }
183 }
184}
185impl CSS {
186 pub fn merge(v: Vec<Self>) -> Vec<Self> {
187 use lightningcss::traits::ToCss;
188 use lightningcss::{
189 printer::PrinterOptions,
190 rules::{CssRule, CssRuleList},
191 selector::Component,
192 stylesheet::{MinifyOptions, ParserOptions, StyleSheet},
193 };
194
195 let mut links = Vec::new();
196 let mut strings = Vec::new();
197 for c in v {
198 match c {
199 Self::Link(_) => links.push(c),
200 Self::Inline(css) | Self::Class { css, .. } => strings.push(css),
201 }
202 }
203
204 let mut sheet = StyleSheet::new(
205 Vec::new(),
206 CssRuleList(Vec::new()),
207 ParserOptions::default(),
208 );
209 let inlines = smallvec::SmallVec::<_, 2>::new();
210 for s in &strings {
211 if let Ok(rs) = StyleSheet::parse(s, ParserOptions::default()) {
212 sheet.rules.0.extend(rs.rules.0.into_iter());
213 } else {
214 tracing::warn!("Not class-able: {s}");
215 }
216 }
217 let _ = sheet.minify(MinifyOptions::default());
218
219 let mut classes = Vec::new();
220 for rule in std::mem::take(&mut sheet.rules.0) {
221 match rule {
222 CssRule::Style(style) => {
223 if style.vendor_prefix.is_empty()
224 && style.selectors.0.len() == 1
225 && style.selectors.0[0].len() == 1
226 && matches!(
227 style.selectors.0[0].iter().next(),
228 Some(Component::Class(_))
229 )
230 {
231 let Some(Component::Class(class_name)) = style.selectors.0[0].iter().next()
232 else {
233 impossible!()
234 };
235 if let Ok(s) = style.to_css_string(PrinterOptions::default()) {
236 classes.push(Self::Class {
237 name: class_name.to_string().into(),
238 css: s.into(),
239 });
240 } else {
241 tracing::warn!("Illegal CSS: {style:?}");
242 }
243 } else {
244 if let Ok(s) = style.to_css_string(PrinterOptions::default()) {
245 tracing::warn!("Not class-able: {s}");
246 links.push(Self::Inline(s.into()));
247 } else {
248 tracing::warn!("Illegal CSS: {style:?}");
249 }
250 }
251 }
252 rule => {
253 if let Ok(s) = rule.to_css_string(PrinterOptions::default()) {
254 tracing::warn!("Not class-able: {s}");
255 links.push(Self::Inline(s.into()));
256 } else {
257 tracing::warn!("Illegal CSS: {rule:?}");
258 }
259 }
260 }
261 }
262 drop(sheet);
263
264 links.extend(inlines.into_iter().map(|i| Self::Inline(strings.remove(i))));
265 links.extend(classes);
266 links
267 }
268
269 #[must_use]
270 pub fn split(css: &str) -> Vec<Self> {
271 use lightningcss::traits::ToCss;
272 use lightningcss::{
273 printer::PrinterOptions,
274 rules::CssRule,
275 selector::Component,
276 stylesheet::{ParserOptions, StyleSheet},
277 };
278 let Ok(ruleset) = StyleSheet::parse(css, ParserOptions::default()) else {
279 tracing::warn!("Not class-able: {css}");
280 return vec![Self::Inline(css.to_string().into())];
281 };
282 if ruleset.sources.iter().any(|s| !s.is_empty()) {
283 tracing::warn!("Not class-able: {css}");
284 return vec![Self::Inline(css.to_string().into())];
285 }
286 ruleset
287 .rules
288 .0
289 .into_iter()
290 .filter_map(|rule| match rule {
291 CssRule::Style(style) => {
292 if style.vendor_prefix.is_empty()
293 && style.selectors.0.len() == 1
294 && style.selectors.0[0].len() == 1
295 && matches!(
296 style.selectors.0[0].iter().next(),
297 Some(Component::Class(_))
298 )
299 {
300 let Some(Component::Class(class_name)) = style.selectors.0[0].iter().next()
301 else {
302 impossible!()
303 };
304 style
305 .to_css_string(PrinterOptions::default())
306 .ok()
307 .map(|s| Self::Class {
308 name: class_name.to_string().into(),
309 css: s.into(),
310 })
311 } else {
312 style
313 .to_css_string(PrinterOptions::default())
314 .ok()
315 .map(|s| {
316 tracing::warn!("Not class-able: {s}");
317 Self::Inline(s.into())
318 })
319 }
320 }
321 o => o.to_css_string(PrinterOptions::default()).ok().map(|s| {
322 tracing::warn!("Not class-able: {s}");
323 Self::Inline(s.into())
324 }),
325 })
326 .collect()
327 }
328}
329 */
330
331#[macro_export]
332macro_rules! impossible {
333 () => {{
334 #[cfg(debug_assertions)]
335 {
336 unreachable!()
337 }
338 #[cfg(not(debug_assertions))]
339 {
340 unsafe { std::hint::unreachable_unchecked() }
341 }
342 }};
343 ($s:literal) => {
344 #[cfg(debug_assertions)]
345 {
346 panic!($s)
347 }
348 #[cfg(not(debug_assertions))]
349 {
350 unsafe { std::hint::unreachable_unchecked() }
351 }
352 };
353 (?) => {
354 unreachable!()
355 };
356 (? $s:literal) => {{
357 panic!($s)
358 }};
359}
360
361#[macro_export]
362macro_rules! unwrap {
363 ($e: expr) => { $e.unwrap_or_else(|| {$crate::impossible!();}) };
364 (? $e: expr) => { $e.unwrap_or_else(|| {$crate::impossible!(?);}) };
365 ($e: expr;$l:literal) => { $e.unwrap_or_else(|| {$crate::impossible!($l);}) };
366 (? $e: expr;$l:literal) => { $e.unwrap_or_else(|| {$crate::impossible!(? $l);}) };
367}
368
369#[cfg(feature = "serde")]
370pub trait CondSerialize: serde::Serialize {}
371#[cfg(feature = "serde")]
372impl<T: serde::Serialize> CondSerialize for T {}
373
374#[cfg(not(feature = "serde"))]
375pub trait CondSerialize {}
376#[cfg(not(feature = "serde"))]
377impl<T> CondSerialize for T {}
378
379/*
380#[allow(clippy::unwrap_used)]
381#[allow(clippy::cognitive_complexity)]
382#[allow(clippy::similar_names)]
383#[test]
384fn css_things() {
385 use lightningcss::traits::ToCss;
386 use lightningcss::{
387 printer::PrinterOptions,
388 rules::CssRule,
389 selector::Component,
390 stylesheet::{MinifyOptions, ParserOptions, StyleSheet},
391 };
392 tracing_subscriber::fmt().init();
393 let css = include_str!("../../../resources/assets/rustex.css");
394 let rules = StyleSheet::parse(css, ParserOptions::default()).unwrap();
395 let roundtrip = rules.to_css(PrinterOptions::default()).unwrap();
396 tracing::info!("{}", roundtrip.code);
397 let test = "
398 .ftml-paragraph {
399 > .ftml-title {
400 font-weight: bold;
401 }
402 margin: 0;
403 }
404 ";
405 let mut ruleset = StyleSheet::parse(test, ParserOptions::default()).unwrap();
406 ruleset.minify(MinifyOptions::default()).unwrap();
407 assert!(ruleset.sources.iter().all(String::is_empty));
408 tracing::info!("Result: {ruleset:#?}");
409 for rule in ruleset.rules.0 {
410 match rule {
411 CssRule::Style(style) => {
412 assert!(style.vendor_prefix.is_empty());
413 assert!(style.selectors.0.len() == 1);
414 assert!(style.selectors.0[0].len() == 1);
415 tracing::info!(
416 "Here: {}",
417 style.to_css_string(PrinterOptions::default()).unwrap()
418 );
419 let sel = style.selectors.0[0].iter().next().unwrap();
420 assert!(matches!(sel, Component::Class(_)));
421 let Component::Class(cls) = sel else {
422 impossible!()
423 };
424 let cls_str = &**cls;
425 tracing::info!("Class: {cls_str}");
426 }
427 o => panic!("Unexpected rule: {o:#?}"),
428 }
429 }
430}
431
432pub trait PathExt {
433 const PATH_SEPARATOR: char;
434 fn as_slash_str(&self) -> String;
435 fn same_fs_as<P: AsRef<std::path::Path>>(&self, other: &P) -> bool;
436 fn rename_safe<P: AsRef<std::path::Path>>(&self, target: &P) -> eyre::Result<()>;
437}
438impl<T: AsRef<std::path::Path>> PathExt for T {
439 #[cfg(target_os = "windows")]
440 const PATH_SEPARATOR: char = '\\';
441 #[cfg(not(target_os = "windows"))]
442 const PATH_SEPARATOR: char = '/';
443 fn as_slash_str(&self) -> String {
444 if cfg!(windows) {
445 unwrap!(self.as_ref().as_os_str().to_str()).replace('\\', "/")
446 } else {
447 unwrap!(self.as_ref().as_os_str().to_str()).to_string()
448 }
449 }
450 #[cfg(target_os = "windows")]
451 fn same_fs_as<P: AsRef<std::path::Path>>(&self, other: &P) -> bool {
452 let Some(p1) = self
453 .as_ref()
454 .components()
455 .next()
456 .and_then(|c| c.as_os_str().to_str())
457 else {
458 return false;
459 };
460 let Some(p2) = other
461 .as_ref()
462 .components()
463 .next()
464 .and_then(|c| c.as_os_str().to_str())
465 else {
466 return false;
467 };
468 p1 == p2
469 }
470 #[cfg(target_arch = "wasm32")]
471 fn same_fs_as<P: AsRef<std::path::Path>>(&self, other: &P) -> bool {
472 impossible!()
473 }
474
475 #[cfg(not(any(target_os = "windows", target_arch = "wasm32")))]
476 fn same_fs_as<P: AsRef<std::path::Path>>(&self, other: &P) -> bool {
477 use std::os::unix::fs::MetadataExt;
478 fn existent_parent(p: &std::path::Path) -> &std::path::Path {
479 if p.exists() {
480 return p;
481 }
482 existent_parent(p.parent().unwrap_or_else(|| unreachable!()))
483 }
484 let p1 = existent_parent(self.as_ref());
485 let p2 = existent_parent(other.as_ref());
486 let md1 = p1.metadata().unwrap_or_else(|_| unreachable!());
487 let md2 = p2.metadata().unwrap_or_else(|_| unreachable!());
488 md1.dev() == md2.dev()
489 }
490 fn rename_safe<P: AsRef<std::path::Path>>(&self, target: &P) -> eyre::Result<()> {
491 Ok(if self.same_fs_as(target) {
492 std::fs::rename(self.as_ref(), target.as_ref())?
493 } else {
494 crate::fs::copy_dir_all(self.as_ref(), target.as_ref())?
495 })
496 }
497}
498 */