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