1#![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;
20pub 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 fn as_hex(&self) -> eyre::Result<String>;
62 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 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}