+ })?;
+ manifest = self.resolve_path(value);
+ }
+ "--dry-run" => dry_run = true,
+ "--help" | "-h" => {
+ print_photo_poison_help();
+ return Err(Box::new(SiteError::new("help requested")));
+ }
+ value if value.starts_with('-') => {
+ return Err(Box::new(SiteError::new(format!(
+ "unknown poison-photos option: {value}"
+ ))));
+ }
+ value => positionals.push(value.to_string()),
+ }
+ index += 1;
+ }
+
+ if !(1..=32).contains(&strength) {
+ return Err(Box::new(SiteError::new(
+ "strength must be between 1 and 32",
+ )));
+ }
+ if !(60..=100).contains(&quality) {
+ return Err(Box::new(SiteError::new(
+ "quality must be between 60 and 100",
+ )));
+ }
+ if positionals.len() > 2 {
+ return Err(Box::new(SiteError::new(
+ "poison-photos accepts at most INPUT and OUTPUT paths",
+ )));
+ }
+
+ Ok(PhotoPoisonOptions {
+ input: positionals.first().map_or_else(
+ || self.root.join("private/photography/originals"),
+ |path| self.resolve_path(path),
+ ),
+ output: positionals.get(1).map_or_else(
+ || self.root.join("content/photography/gallery"),
+ |path| self.resolve_path(path),
+ ),
+ manifest,
+ strength,
+ quality,
+ dry_run,
+ })
+ }
+
+ fn resolve_path(&self, path: &str) -> PathBuf {
+ let path = PathBuf::from(path);
+ if path.is_absolute() {
+ path
+ } else {
+ self.root.join(path)
+ }
+ }
+
+ fn ensure_photo_input_is_private(&self, path: &Path) -> Result<()> {
+ let normalized_root = normalize_path(&self.root)?;
+ let normalized_path = normalize_path(path)?;
+ let content_dir = normalized_root.join("content");
+
+ if normalized_path.starts_with(&content_dir) {
+ return Err(Box::new(SiteError::new(format!(
+ "photo originals must not live under {}; use private/photography/originals or another untracked directory",
+ content_dir.display()
+ ))));
+ }
+
+ Ok(())
+ }
+
fn dates(&self, target: &str) -> Result<()> {
if self.ci {
let shallow = self.output_optional(
@@ -406,14 +757,8 @@ impl Site {
self.run_pnpm(dir, &os_args(&["update"]))?;
self.run_pnpm(dir, &os_args(&["audit", "fix"]))?;
self.pnpm_install(dir, InstallMode::Unlocked)?;
- self.run_pnpm(
- dir,
- &os_args(&["exec", "prettier", lint_target, "--write"]),
- )?;
- self.run_pnpm(
- dir,
- &os_args(&["exec", "eslint", lint_target, "--fix"]),
- )
+ self.run_pnpm(dir, &os_args(&["exec", "prettier", lint_target, "--write"]))?;
+ self.run_pnpm(dir, &os_args(&["exec", "eslint", lint_target, "--fix"]))
}
fn cargo_update(&self, dir: &Path) -> Result<()> {
@@ -605,6 +950,30 @@ impl Site {
self.run_with_env(cwd, program, args, &[])
}
+ fn spawn(&self, cwd: &Path, program: P, args: &[OsString]) -> Result
+ where
+ P: AsRef,
+ {
+ let program = program.as_ref();
+ self.print_command(cwd, program, args);
+
+ let child = Command::new(program)
+ .args(args)
+ .current_dir(cwd)
+ .spawn()
+ .map_err(|source| {
+ SiteError(format!(
+ "failed to run {}: {source}",
+ format_command(program, args)
+ ))
+ })?;
+
+ Ok(ChildGuard {
+ child,
+ label: format_command(program, args),
+ })
+ }
+
fn run_with_env(
&self,
cwd: &Path,
@@ -740,8 +1109,11 @@ fn run_main() -> Result<()> {
report::write(&site, &site.root.join("public"), build_time)
}
"align-tables" => tables::align(&args[1..]),
+ "recursive-ji-music" | "rji-music" => recursive_ji::generate(&site, &args[1..]),
"aoc-problems" | "download-aoc-problems" => aoc::download_problem_text(&site, &args[1..]),
"aoc-inputs" | "download-aoc-inputs" => aoc::download_inputs(&site, &args[1..]),
+ "poison-photos" | "photo-poison" => site.poison_photos(&args[1..]),
+ "check" => site.check(),
"dates" => {
let target = args.get(1).map_or("content", String::as_str);
site.dates(target)
@@ -793,14 +1165,20 @@ Usage:
Commands:
build [--dev|--prod] build wasm assets and the Quartz site
+ --dev also watches TypeScript bundles
wasm [--dev|--prod] build only the wasm and TypeScript bundle
generate regenerate link lists, indices, chords, and dates
links regenerate plaintext link lists
indices regenerate misc indices and trolley count
report [seconds] write public/report.html build report
align-tables LEFT RIGHT SEP merge matching lines from two files
+ recursive-ji-music [OUTPUT]
+ render recursive just-intonation examples
aoc-problems [options] download scaffolded AoC problem statements
aoc-inputs [options] download scaffolded AoC puzzle inputs
+ poison-photos [INPUT] [OUTPUT]
+ protect source photos and update gallery manifest
+ check run Rust, TypeScript, and lint checks
dates [path] refresh markdown date metadata
update run dependency updates and linters
commit [message] CI-only commit and push for generated files
@@ -808,6 +1186,27 @@ Commands:
);
}
+fn print_photo_poison_help() {
+ println!(
+ "\
+poison-photos
+
+Usage:
+ cargo run --manifest-path tools/site/Cargo.toml -- poison-photos [INPUT] [OUTPUT] [options]
+
+Defaults:
+ INPUT private/photography/originals
+ OUTPUT content/photography/gallery
+
+Options:
+ --strength N perturbation strength, 1-32, default 4
+ --quality N JPEG quality, 60-100, default 92
+ --manifest PATH gallery JSON path, default content/photography/gallery.json
+ --dry-run print planned work without writing images
+"
+ );
+}
+
fn find_repo_root() -> Result {
let mut current = env::current_dir()?;
@@ -853,6 +1252,239 @@ fn remove_license_files(path: &Path) -> Result<()> {
Ok(())
}
+fn normalize_path(path: &Path) -> Result {
+ if path.exists() {
+ Ok(path.canonicalize()?)
+ } else if let Some(parent) = path.parent() {
+ let parent = normalize_path(parent)?;
+ Ok(parent.join(
+ path.file_name()
+ .ok_or_else(|| SiteError::new(format!("invalid path: {}", path.display())))?,
+ ))
+ } else {
+ Ok(path.to_path_buf())
+ }
+}
+
+fn collect_photo_files(dir: &Path, files: &mut Vec) -> Result<()> {
+ if !dir.exists() {
+ return Ok(());
+ }
+
+ for entry in fs::read_dir(dir)? {
+ let entry = entry?;
+ let path = entry.path();
+ if path.is_dir() {
+ collect_photo_files(&path, files)?;
+ } else if is_photo_file(&path) {
+ files.push(path);
+ }
+ }
+
+ Ok(())
+}
+
+fn is_photo_file(path: &Path) -> bool {
+ let Some(extension) = path.extension().and_then(OsStr::to_str) else {
+ return false;
+ };
+
+ matches!(
+ extension.to_ascii_lowercase().as_str(),
+ "jpg" | "jpeg" | "png" | "webp"
+ )
+}
+
+fn poison_photo_file(
+ input: &Path,
+ output: &Path,
+ seed: u64,
+ strength: u8,
+ quality: u8,
+) -> Result<(u32, u32)> {
+ let image = ImageReader::open(input)?.with_guessed_format()?.decode()?;
+ let source = image.to_rgba8();
+ let (width, height) = source.dimensions();
+ let poisoned = poison_pixels(&source, seed, strength);
+
+ let file = File::create(output)?;
+ let writer = BufWriter::new(file);
+ let mut encoder = JpegEncoder::new_with_quality(writer, quality);
+ encoder.encode_image(&poisoned)?;
+
+ Ok((width, height))
+}
+
+fn poison_pixels(source: &image::RgbaImage, seed: u64, strength: u8) -> RgbImage {
+ let (width, height) = source.dimensions();
+ let mut rgb = Vec::with_capacity((width * height) as usize);
+ let mut luma = Vec::with_capacity((width * height) as usize);
+
+ for pixel in source.pixels() {
+ let alpha = f32::from(pixel[3]) / 255.0;
+ let color = [
+ f32::from(pixel[0]) * alpha + 255.0 * (1.0 - alpha),
+ f32::from(pixel[1]) * alpha + 255.0 * (1.0 - alpha),
+ f32::from(pixel[2]) * alpha + 255.0 * (1.0 - alpha),
+ ];
+ luma.push(color[0] * 0.2126 + color[1] * 0.7152 + color[2] * 0.0722);
+ rgb.push(color);
+ }
+
+ let mut output = RgbImage::new(width, height);
+ for y in 0..height {
+ for x in 0..width {
+ let index = (y * width + x) as usize;
+ let edge = local_edge_strength(&luma, width, height, x, y);
+ let amplitude = f32::from(strength) * (0.45 + edge * 0.75);
+ let mut color = [0_u8; 3];
+
+ for channel in 0..3 {
+ let channel_seed = seed
+ ^ u64::from(x).wrapping_mul(0x9E37_79B1_85EB_CA87)
+ ^ u64::from(y).wrapping_mul(0xC2B2_AE3D_27D4_EB4F)
+ ^ (channel as u64).wrapping_mul(0x1656_67B1_9E37_79F9);
+ let noise = signed_noise(channel_seed);
+ let checker = if ((x ^ y ^ channel as u32) & 1) == 0 {
+ 1.0
+ } else {
+ -1.0
+ };
+ let wave = ((x as f32 * 0.73
+ + y as f32 * 1.37
+ + channel as f32 * 2.11
+ + (seed & 1023) as f32)
+ .sin())
+ * 0.35;
+ let delta = amplitude * (noise * 0.65 + checker * 0.18 + wave);
+ color[channel] = (rgb[index][channel] + delta).clamp(0.0, 255.0).round() as u8;
+ }
+
+ output.put_pixel(x, y, Rgb(color));
+ }
+ }
+
+ output
+}
+
+fn local_edge_strength(luma: &[f32], width: u32, height: u32, x: u32, y: u32) -> f32 {
+ let center = luma[(y * width + x) as usize];
+ let mut total = 0.0;
+ let mut count = 0.0;
+ let neighbors = [
+ (x.checked_sub(1), Some(y)),
+ (x.checked_add(1).filter(|value| *value < width), Some(y)),
+ (Some(x), y.checked_sub(1)),
+ (Some(x), y.checked_add(1).filter(|value| *value < height)),
+ ];
+
+ for (nx, ny) in neighbors {
+ if let (Some(nx), Some(ny)) = (nx, ny) {
+ total += (center - luma[(ny * width + nx) as usize]).abs();
+ count += 1.0;
+ }
+ }
+
+ if count == 0.0 {
+ 0.0
+ } else {
+ (total / count / 255.0).clamp(0.0, 1.0)
+ }
+}
+
+fn signed_noise(seed: u64) -> f32 {
+ let value = splitmix64(seed);
+ let unit = (value >> 11) as f64 / ((1_u64 << 53) as f64);
+ (unit as f32) * 2.0 - 1.0
+}
+
+fn splitmix64(mut value: u64) -> u64 {
+ value = value.wrapping_add(0x9E37_79B9_7F4A_7C15);
+ value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
+ value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
+ value ^ (value >> 31)
+}
+
+fn hash64(bytes: &[u8]) -> u64 {
+ let mut hash = 0xCBF2_9CE4_8422_2325_u64;
+ for byte in bytes {
+ hash ^= u64::from(*byte);
+ hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
+ }
+ hash
+}
+
+fn sanitize_file_stem(stem: &str) -> String {
+ let mut result = String::new();
+ for ch in stem.chars() {
+ if ch.is_ascii_alphanumeric() {
+ result.push(ch.to_ascii_lowercase());
+ } else if !result.ends_with('-') {
+ result.push('-');
+ }
+ }
+
+ let result = result.trim_matches('-');
+ if result.is_empty() {
+ "photo".to_string()
+ } else {
+ result.to_string()
+ }
+}
+
+fn title_from_stem(stem: &str) -> String {
+ let title = stem
+ .replace(['_', '-'], " ")
+ .split_whitespace()
+ .collect::>()
+ .join(" ");
+
+ if title.is_empty() {
+ "untitled photo".to_string()
+ } else {
+ title
+ }
+}
+
+fn write_gallery_manifest(path: &Path, entries: &[GalleryEntry]) -> Result<()> {
+ if let Some(parent) = path.parent() {
+ fs::create_dir_all(parent)?;
+ }
+
+ let mut body = String::from("[\n");
+ for (index, entry) in entries.iter().enumerate() {
+ let comma = if index + 1 == entries.len() { "" } else { "," };
+ body.push_str(" {\n");
+ body.push_str(&format!(" \"src\": {},\n", json_string(&entry.src)));
+ body.push_str(&format!(" \"title\": {},\n", json_string(&entry.title)));
+ body.push_str(&format!(" \"meta\": {},\n", json_string(&entry.meta)));
+ body.push_str(&format!(" \"width\": {},\n", entry.width));
+ body.push_str(&format!(" \"height\": {}\n", entry.height));
+ body.push_str(&format!(" }}{comma}\n"));
+ }
+ body.push_str("]\n");
+
+ fs::write(path, body)?;
+ Ok(())
+}
+
+fn json_string(value: &str) -> String {
+ let mut result = String::from("\"");
+ for ch in value.chars() {
+ match ch {
+ '"' => result.push_str("\\\""),
+ '\\' => result.push_str("\\\\"),
+ '\n' => result.push_str("\\n"),
+ '\r' => result.push_str("\\r"),
+ '\t' => result.push_str("\\t"),
+ ch if ch.is_control() => result.push_str(&format!("\\u{:04x}", ch as u32)),
+ ch => result.push(ch),
+ }
+ }
+ result.push('"');
+ result
+}
+
fn existing_date_metadata(path: &Path) -> Result> {
if !path.exists() {
return Ok(Vec::new());
diff --git a/tools/site/src/recursive_ji.rs b/tools/site/src/recursive_ji.rs
new file mode 100644
index 00000000..864dd206
--- /dev/null
+++ b/tools/site/src/recursive_ji.rs
@@ -0,0 +1,442 @@
+use std::f64::consts::TAU;
+use std::fs;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+
+use crate::{Result, Site, SiteError};
+
+const SAMPLE_RATE: u32 = 44_100;
+const C4_HZ: f64 = 261.625_565_300_598_6;
+const DEFAULT_OUTPUT_DIR: &str = "content/blog/recursive-just-intonation";
+const NOTE_NAMES: [&str; 12] = [
+ "C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B",
+];
+const JUST_C_SCALE: [Ratio; 12] = [
+ Ratio::new(1, 1),
+ Ratio::new(17, 16),
+ Ratio::new(9, 8),
+ Ratio::new(19, 16),
+ Ratio::new(5, 4),
+ Ratio::new(4, 3),
+ Ratio::new(45, 32),
+ Ratio::new(3, 2),
+ Ratio::new(51, 32),
+ Ratio::new(27, 16),
+ Ratio::new(57, 32),
+ Ratio::new(15, 8),
+];
+
+#[derive(Clone, Copy)]
+struct Ratio {
+ numerator: u32,
+ denominator: u32,
+}
+
+#[derive(Clone, Copy)]
+enum ChordKind {
+ Major,
+ Dominant,
+}
+
+#[derive(Clone, Copy)]
+enum Tuning {
+ TwelveTet,
+ FixedCJust,
+ RecursiveJust,
+}
+
+#[derive(Clone, Copy)]
+struct Chord {
+ name: &'static str,
+ root_pc: i32,
+ octave_shift: i32,
+ kind: ChordKind,
+}
+
+#[derive(Clone, Copy)]
+struct NoteEvent {
+ frequency: f64,
+ start: f64,
+ duration: f64,
+ amplitude: f64,
+}
+
+impl Ratio {
+ const fn new(numerator: u32, denominator: u32) -> Self {
+ Self {
+ numerator,
+ denominator,
+ }
+ }
+
+ fn as_f64(self) -> f64 {
+ f64::from(self.numerator) / f64::from(self.denominator)
+ }
+}
+
+impl ChordKind {
+ fn offsets(self) -> &'static [i32] {
+ match self {
+ ChordKind::Major => &[0, 4, 7],
+ ChordKind::Dominant => &[0, 4, 7, 10],
+ }
+ }
+}
+
+impl Tuning {
+ fn file_stem(self) -> &'static str {
+ match self {
+ Tuning::TwelveTet => "twelve-tet-progression",
+ Tuning::FixedCJust => "fixed-c-ji-progression",
+ Tuning::RecursiveJust => "recursive-ji-progression",
+ }
+ }
+
+ fn label(self) -> &'static str {
+ match self {
+ Tuning::TwelveTet => "12-TET",
+ Tuning::FixedCJust => "Fixed C just intonation",
+ Tuning::RecursiveJust => "Recursive just intonation",
+ }
+ }
+}
+
+pub(crate) fn generate(site: &Site, args: &[String]) -> Result<()> {
+ if args.iter().any(|arg| arg == "--help" || arg == "-h") {
+ print_help();
+ return Ok(());
+ }
+
+ if args.len() > 1 {
+ return Err(Box::new(SiteError::new(
+ "recursive-ji-music accepts at most one output directory",
+ )));
+ }
+
+ let output_dir = args.first().map_or_else(
+ || site.root.join(DEFAULT_OUTPUT_DIR),
+ |path| site.root.join(path),
+ );
+ fs::create_dir_all(&output_dir)?;
+
+ for tuning in [Tuning::TwelveTet, Tuning::FixedCJust, Tuning::RecursiveJust] {
+ let samples = render_progression(tuning);
+ let path = output_dir.join(format!("{}.wav", tuning.file_stem()));
+ write_wav(&path, &samples)?;
+ println!("wrote {}", relative_to_root(site, &path).display());
+ }
+
+ let split_samples = render_note_splits();
+ let split_path = output_dir.join("recursive-ji-note-splits.wav");
+ write_wav(&split_path, &split_samples)?;
+ println!("wrote {}", relative_to_root(site, &split_path).display());
+
+ let csv_path = output_dir.join("recursive-ji-frequencies.csv");
+ fs::write(&csv_path, frequency_report())?;
+ println!("wrote {}", relative_to_root(site, &csv_path).display());
+
+ Ok(())
+}
+
+fn render_progression(tuning: Tuning) -> Vec {
+ let chords = progression();
+ let mut events = Vec::new();
+ let mut cursor = 0.0;
+
+ for chord in chords {
+ add_chord_events(&mut events, chord, tuning, cursor, 1.8);
+ cursor += 1.8;
+ }
+
+ synthesize(&events, cursor + 0.4)
+}
+
+fn render_note_splits() -> Vec {
+ let pairs = [
+ ("G#/Ab", Chord::major("E", 4), 4),
+ ("A", Chord::major("F", 5), 4),
+ ("C#/Db", Chord::major("A", 9), 4),
+ ("F", Chord::dominant("G7", 7), 10),
+ ];
+ let mut events = Vec::new();
+ let mut cursor = 0.0;
+
+ for (_name, chord, offset) in pairs {
+ let recursive = note_frequency(Tuning::RecursiveJust, chord, offset);
+ let fixed = note_frequency(Tuning::FixedCJust, chord, offset);
+ events.push(NoteEvent {
+ frequency: fixed,
+ start: cursor,
+ duration: 0.85,
+ amplitude: 0.4,
+ });
+ events.push(NoteEvent {
+ frequency: recursive,
+ start: cursor + 0.9,
+ duration: 0.85,
+ amplitude: 0.4,
+ });
+ events.push(NoteEvent {
+ frequency: fixed,
+ start: cursor + 1.9,
+ duration: 1.2,
+ amplitude: 0.22,
+ });
+ events.push(NoteEvent {
+ frequency: recursive,
+ start: cursor + 1.9,
+ duration: 1.2,
+ amplitude: 0.22,
+ });
+ cursor += 3.4;
+ }
+
+ synthesize(&events, cursor + 0.4)
+}
+
+fn add_chord_events(
+ events: &mut Vec,
+ chord: Chord,
+ tuning: Tuning,
+ start: f64,
+ duration: f64,
+) {
+ let offsets = chord.kind.offsets();
+
+ for (index, offset) in offsets.iter().enumerate() {
+ events.push(NoteEvent {
+ frequency: note_frequency(tuning, chord, *offset),
+ start: start + index as f64 * 0.15,
+ duration: duration - index as f64 * 0.08,
+ amplitude: 0.28,
+ });
+ }
+
+ for offset in offsets {
+ events.push(NoteEvent {
+ frequency: note_frequency(tuning, chord, *offset + 12),
+ start: start + 0.55,
+ duration: duration * 0.62,
+ amplitude: 0.11,
+ });
+ }
+}
+
+fn progression() -> [Chord; 12] {
+ [
+ Chord::major("C", 0),
+ Chord::major("E", 4),
+ Chord::major("G#/Ab", 8),
+ Chord::major("C", 0),
+ Chord::major("F", 5),
+ Chord::major("A", 9),
+ Chord::major("D", 2),
+ Chord::dominant("G7", 7),
+ Chord::major("C", 0),
+ Chord::major("E", 4),
+ Chord::major("F", 5),
+ Chord::major("C", 0),
+ ]
+}
+
+impl Chord {
+ fn major(name: &'static str, root_pc: i32) -> Self {
+ Self {
+ name,
+ root_pc,
+ octave_shift: -1,
+ kind: ChordKind::Major,
+ }
+ }
+
+ fn dominant(name: &'static str, root_pc: i32) -> Self {
+ Self {
+ name,
+ root_pc,
+ octave_shift: -1,
+ kind: ChordKind::Dominant,
+ }
+ }
+}
+
+fn note_frequency(tuning: Tuning, chord: Chord, offset: i32) -> f64 {
+ match tuning {
+ Tuning::TwelveTet => {
+ let semitones_from_c4 = chord.octave_shift * 12 + chord.root_pc + offset;
+ C4_HZ * 2.0_f64.powf(f64::from(semitones_from_c4) / 12.0)
+ }
+ Tuning::FixedCJust => fixed_c_frequency(chord.octave_shift, chord.root_pc + offset),
+ Tuning::RecursiveJust => {
+ let root = fixed_c_frequency(chord.octave_shift, chord.root_pc);
+ root * just_ratio_for_interval(offset)
+ }
+ }
+}
+
+fn fixed_c_frequency(octave_shift: i32, absolute_index: i32) -> f64 {
+ let octave = octave_shift + absolute_index.div_euclid(12);
+ let pc = absolute_index.rem_euclid(12) as usize;
+ C4_HZ * JUST_C_SCALE[pc].as_f64() * 2.0_f64.powi(octave)
+}
+
+fn just_ratio_for_interval(offset: i32) -> f64 {
+ let octave = offset.div_euclid(12);
+ let pc = offset.rem_euclid(12) as usize;
+ JUST_C_SCALE[pc].as_f64() * 2.0_f64.powi(octave)
+}
+
+fn synthesize(events: &[NoteEvent], duration: f64) -> Vec {
+ let sample_count = (duration * f64::from(SAMPLE_RATE)).ceil() as usize;
+ let mut buffer = vec![0.0_f64; sample_count];
+
+ for event in events {
+ let start_index = (event.start * f64::from(SAMPLE_RATE)).max(0.0) as usize;
+ let end_index = ((event.start + event.duration) * f64::from(SAMPLE_RATE))
+ .ceil()
+ .min(sample_count as f64) as usize;
+
+ for (index, sample) in buffer
+ .iter_mut()
+ .enumerate()
+ .take(end_index)
+ .skip(start_index)
+ {
+ let local_t = index as f64 / f64::from(SAMPLE_RATE) - event.start;
+ let phase = TAU * event.frequency * local_t;
+ let tone = phase.sin() * 0.78 + (phase * 2.0).sin() * 0.17 + (phase * 3.0).sin() * 0.05;
+ *sample += tone * envelope(local_t, event.duration) * event.amplitude;
+ }
+ }
+
+ normalize(buffer)
+}
+
+fn envelope(t: f64, duration: f64) -> f64 {
+ let attack = 0.025;
+ let release = 0.18_f64.min(duration * 0.35);
+ if t < attack {
+ t / attack
+ } else if t > duration - release {
+ ((duration - t) / release).clamp(0.0, 1.0)
+ } else {
+ let sustain_fade = 1.0 - 0.35 * (t / duration);
+ sustain_fade.clamp(0.0, 1.0)
+ }
+}
+
+fn normalize(buffer: Vec) -> Vec {
+ let peak = buffer.iter().copied().map(f64::abs).fold(0.0_f64, f64::max);
+ let gain = if peak > 0.0 { 0.86 / peak } else { 1.0 };
+
+ buffer
+ .into_iter()
+ .map(|sample| (sample * gain).clamp(-1.0, 1.0) as f32)
+ .collect()
+}
+
+fn write_wav(path: &Path, samples: &[f32]) -> Result<()> {
+ let mut file = fs::File::create(path)?;
+ let channels = 1_u16;
+ let bits_per_sample = 16_u16;
+ let block_align = channels * bits_per_sample / 8;
+ let byte_rate = SAMPLE_RATE * u32::from(block_align);
+ let data_len = samples.len() as u32 * u32::from(block_align);
+
+ file.write_all(b"RIFF")?;
+ file.write_all(&(36 + data_len).to_le_bytes())?;
+ file.write_all(b"WAVE")?;
+ file.write_all(b"fmt ")?;
+ file.write_all(&16_u32.to_le_bytes())?;
+ file.write_all(&1_u16.to_le_bytes())?;
+ file.write_all(&channels.to_le_bytes())?;
+ file.write_all(&SAMPLE_RATE.to_le_bytes())?;
+ file.write_all(&byte_rate.to_le_bytes())?;
+ file.write_all(&block_align.to_le_bytes())?;
+ file.write_all(&bits_per_sample.to_le_bytes())?;
+ file.write_all(b"data")?;
+ file.write_all(&data_len.to_le_bytes())?;
+
+ for sample in samples {
+ let pcm = (sample.clamp(-1.0, 1.0) * f32::from(i16::MAX)).round() as i16;
+ file.write_all(&pcm.to_le_bytes())?;
+ }
+
+ Ok(())
+}
+
+fn frequency_report() -> String {
+ let mut body =
+ String::from("tuning,chord,note,frequency_hz,cents_vs_12_tet,cents_vs_fixed_c_ji\n");
+
+ for chord in progression() {
+ for offset in chord.kind.offsets() {
+ for tuning in [Tuning::TwelveTet, Tuning::FixedCJust, Tuning::RecursiveJust] {
+ let frequency = note_frequency(tuning, chord, *offset);
+ let tet = note_frequency(Tuning::TwelveTet, chord, *offset);
+ let fixed = note_frequency(Tuning::FixedCJust, chord, *offset);
+ body.push_str(&format!(
+ "{},{},{},{:.3},{:.3},{:.3}\n",
+ tuning.label(),
+ chord.name,
+ NOTE_NAMES[(chord.root_pc + *offset).rem_euclid(12) as usize],
+ frequency,
+ cents_between(frequency, tet),
+ cents_between(frequency, fixed),
+ ));
+ }
+ }
+ }
+
+ body
+}
+
+fn cents_between(a: f64, b: f64) -> f64 {
+ 1200.0 * (a / b).log2()
+}
+
+fn relative_to_root(site: &Site, path: &Path) -> PathBuf {
+ path.strip_prefix(&site.root).unwrap_or(path).to_path_buf()
+}
+
+fn print_help() {
+ println!(
+ "\
+recursive-ji-music
+
+Usage:
+ cargo run --manifest-path tools/site/Cargo.toml -- recursive-ji-music [OUTPUT]
+
+Defaults:
+ OUTPUT content/blog/recursive-just-intonation
+
+Writes:
+ twelve-tet-progression.wav
+ fixed-c-ji-progression.wav
+ recursive-ji-progression.wav
+ recursive-ji-note-splits.wav
+ recursive-ji-frequencies.csv
+"
+ );
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn recursive_e_major_third_is_not_fixed_c_g_sharp() {
+ let chord = Chord::major("E", 4);
+ let recursive = note_frequency(Tuning::RecursiveJust, chord, 4);
+ let fixed = note_frequency(Tuning::FixedCJust, chord, 4);
+
+ assert!((cents_between(recursive, fixed) + 34.282).abs() < 0.01);
+ }
+
+ #[test]
+ fn renders_audio() {
+ let samples = render_progression(Tuning::RecursiveJust);
+
+ assert!(samples.len() > SAMPLE_RATE as usize);
+ assert!(samples.iter().any(|sample| sample.abs() > 0.01));
+ }
+}
diff --git a/ts/package.json b/ts/package.json
index b1314487..15bfbf69 100644
--- a/ts/package.json
+++ b/ts/package.json
@@ -7,10 +7,11 @@
"scripts": {
"serve_release": "webpack-dev-server --config webpack.config.mjs --mode production",
"serve_dev": "webpack-dev-server --config webpack.config.mjs --mode development",
- "build:glsl2hlsl": "bun build dist/glsl.js --outfile=../../../content/tools/glsl2hlsl/glsl2hlsl.js --minify --drop=console",
- "build:aoc": "bun build dist/aoc.js --outfile=../../../content/tools/aoc/aoc.js --minify --drop=console",
- "build:tuningplayground": "bun build dist/tuningplayground.js --outfile=../../../content/tools/tuningplayground/tuningplayground.js --minify --drop=console",
- "build:textprocessing": "bun build dist/textprocessing.js --outfile=../../../content/tools/textprocessing/textprocessing.js --minify --drop=console"
+ "check": "tsc --noEmit --incremental false && eslint src",
+ "format": "prettier src --write && eslint src --fix",
+ "watch": "tsc && run-p watch:ts watch:webpack",
+ "watch:ts": "tsc --watch --preserveWatchOutput",
+ "watch:webpack": "webpack --config webpack.config.mjs --mode development --watch"
},
"repository": {
"type": "git",
diff --git a/ts/src/aoc/ui.ts b/ts/src/aoc/ui.ts
index 5da0c648..fa6277dc 100644
--- a/ts/src/aoc/ui.ts
+++ b/ts/src/aoc/ui.ts
@@ -110,7 +110,10 @@ export function createTabs(container: HTMLElement, config: TabConfig) {
descriptionArea.className = "big-field description-field"
descriptionArea.value = retrieve_problem(y, d, p)
descriptionArea.disabled = true
- descriptionArea.setAttribute("aria-label", `Problem description for ${y} day ${d} part ${p}`)
+ descriptionArea.setAttribute(
+ "aria-label",
+ `Problem description for ${y} day ${d} part ${p}`,
+ )
const codeArea = document.createElement("div")
codeArea.className = "big-field code-field"
@@ -258,10 +261,7 @@ export function createTabs(container: HTMLElement, config: TabConfig) {
const percentage = aoc_completion_percentage(completeCount, totalProblems)
btn.textContent = `${year} ${percentage === 100 ? STAR : `${percentage}%`}`
btn.classList.toggle("active", year === activeYear)
- btn.setAttribute(
- "aria-label",
- `${year}: ${completeCount} of ${totalProblems} stars complete`,
- )
+ btn.setAttribute("aria-label", `${year}: ${completeCount} of ${totalProblems} stars complete`)
btn.setAttribute("aria-pressed", String(year === activeYear))
})
}
@@ -289,7 +289,9 @@ export function createTabs(container: HTMLElement, config: TabConfig) {
if (meta) {
meta.textContent =
- completeCount === totalProblems ? STAR.repeat(totalProblems) : `${completeCount}/${totalProblems}`
+ completeCount === totalProblems
+ ? STAR.repeat(totalProblems)
+ : `${completeCount}/${totalProblems}`
}
})
@@ -314,10 +316,7 @@ export function createTabs(container: HTMLElement, config: TabConfig) {
const isComplete = complete[activeYear - START_YEAR][activeDay - 1][problem - 1]
btn.textContent = `part ${problem}${isComplete ? ` ${STAR}` : ""}`
- btn.setAttribute(
- "aria-label",
- `Part ${problem}${isComplete ? ", complete" : ", incomplete"}`,
- )
+ btn.setAttribute("aria-label", `Part ${problem}${isComplete ? ", complete" : ", incomplete"}`)
btn.setAttribute("aria-pressed", String(problem === activeProblem))
})
}
diff --git a/ts/src/bayestheorem.ts b/ts/src/bayestheorem.ts
index ecd9a649..d18f9199 100644
--- a/ts/src/bayestheorem.ts
+++ b/ts/src/bayestheorem.ts
@@ -1,4 +1,4 @@
-import { solve_bayes_percent } from "wasm"
+import { bayes_number, solve_bayes_percent } from "wasm"
type BayesMode = "computed-evidence" | "known-evidence"
@@ -7,13 +7,7 @@ type BayesEventLabels = {
eventB: string
}
-type BayesColorRole =
- | "posterior"
- | "likelihood"
- | "prior"
- | "evidence"
- | "numerator"
- | "neutral"
+type BayesColorRole = "posterior" | "likelihood" | "prior" | "evidence" | "numerator" | "neutral"
type InlineContent = string | Node
type BayesFormulaValues = {
prior: number
@@ -53,7 +47,7 @@ function renderBayesSolver() {
const heading = document.createElement("h2")
heading.id = "bayes-solver-heading"
- heading.textContent = "bayes theorem solver"
+ heading.textContent = "solver"
const form = document.createElement("form")
form.className = "bayes-solver-form"
@@ -61,14 +55,8 @@ function renderBayesSolver() {
const eventGrid = document.createElement("div")
eventGrid.className = "bayes-event-fields"
- const eventA = createEventField(
- "A",
- "event A",
- "a person has the disease",
- "a person has the disease",
- )
+ const eventA = createEventField("event A", "a person has the disease", "a person has the disease")
const eventB = createEventField(
- "B",
"event B",
"the screening test is positive",
"the screening test is positive",
@@ -82,29 +70,34 @@ function renderBayesSolver() {
fieldGrid.className = "bayes-solver-fields"
const prior = createPercentField(
- [bayesTerm("P(A)", "prior")],
- [bayesTerm("P(A)", "prior"), ", the prior probability of event A"],
+ [bayesTerm("P(A)", "prior"), ", ", bayesPhrase("the prior probability of event A", "prior")],
"1",
"prior",
)
const likelihood = createPercentField(
- [bayesTerm("P(B | A)", "likelihood")],
- [bayesTerm("P(B | A)", "likelihood"), ", the likelihood of event B given event A"],
+ [
+ bayesTerm("P(B | A)", "likelihood"),
+ ", ",
+ bayesPhrase("the likelihood of event B given event A", "likelihood"),
+ ],
"90",
"likelihood",
)
const falsePositive = createPercentField(
- [bayesTerm("P(B | not A)", "neutral")],
[
bayesTerm("P(B | not A)", "neutral"),
- ", the probability of event B given event A did not occur",
+ ", ",
+ bayesPhrase("the probability of event B given event A did not occur", "neutral"),
],
"5",
"neutral",
)
const evidence = createPercentField(
- [bayesTerm("P(B)", "evidence")],
- [bayesTerm("P(B)", "evidence"), ", the marginal probability of event B"],
+ [
+ bayesTerm("P(B)", "evidence"),
+ ", ",
+ bayesPhrase("the marginal probability of event B", "evidence"),
+ ],
"5.85",
"evidence",
)
@@ -131,49 +124,51 @@ function renderBayesSolver() {
const output = document.createElement("div")
output.className = "bayes-solver-output"
- output.setAttribute("aria-live", "polite")
const posterior = createOutputItem(
- [bayesTerm("P(A | B)", "posterior")],
[
bayesTerm("P(A | B)", "posterior"),
- ", the probability of event A occurring given event B has occurred",
+ ", ",
+ bayesPhrase("the probability of event A occurring given event B has occurred", "posterior"),
],
"posterior",
"posterior",
)
const numerator = createOutputItem(
- [bayesTerm("P(B | A)", "likelihood"), " * ", bayesTerm("P(A)", "prior")],
[
bayesTerm("P(B | A)", "likelihood"),
" times ",
bayesTerm("P(A)", "prior"),
- ", likelihood times prior",
+ ", ",
+ bayesPhrase("likelihood times prior", "numerator"),
],
"numerator",
"numerator",
)
const evidenceOutput = createOutputItem(
- [bayesTerm("P(B)", "evidence")],
- [bayesTerm("P(B)", "evidence"), ", the marginal probability of event B"],
+ [
+ bayesTerm("P(B)", "evidence"),
+ ", ",
+ bayesPhrase("the marginal probability of event B", "evidence"),
+ ],
"evidence",
"evidence",
)
const odds = createOutputItem(
- ["odds"],
- ["posterior odds from ", bayesTerm("P(A | B)", "posterior")],
+ [bayesPhrase("posterior odds", "posterior"), " from ", bayesTerm("P(A | B)", "posterior")],
"odds",
"posterior",
)
output.append(posterior.element, numerator.element, evidenceOutput.element, odds.element)
- const latex = createLatexOutput()
+
+ const solution = createSolutionOutput()
const error = document.createElement("p")
error.className = "bayes-solver-error"
error.setAttribute("role", "status")
- form.append(eventGrid, definition, fieldGrid, modeLabel, output, latex.element, error)
+ form.append(eventGrid, definition, fieldGrid, modeLabel, output, solution.element, error)
solverRoot.append(heading, form)
function getMode(): BayesMode {
@@ -187,64 +182,56 @@ function renderBayesSolver() {
}
}
- function updateCopy(labels: BayesEventLabels) {
+ function updateCopy(labels: BayesEventLabels, values: BayesFormulaValues) {
const notEventA = `it is not true that ${labels.eventA}`
- const priorPercent = percentInputText(prior.input)
- const likelihoodPercent = percentInputText(likelihood.input)
- const falsePositivePercent = percentInputText(falsePositive.input)
+ const priorPercent = formatProbabilityPercent(values.prior)
+ const likelihoodPercent = formatProbabilityPercent(values.likelihood)
+ const evidencePercent = formatProbabilityPercent(values.evidence)
+ const posteriorPercent = formatPosteriorPercent(values)
setInlineContent(
definition,
- "Example: ",
- priorPercent,
- " is ",
- bayesTerm("P(A)", "prior"),
- ", the prior probability that ",
- labels.eventA,
- "; ",
- likelihoodPercent,
- " is ",
- bayesTerm("P(B | A)", "likelihood"),
- ", the likelihood that ",
- labels.eventB,
- " given that ",
- labels.eventA,
- "; and ",
- falsePositivePercent,
- " is ",
- bayesTerm("P(B | not A)", "neutral"),
- ", the probability that ",
- labels.eventB,
- " given that ",
- notEventA,
- ". The solver finds ",
- bayesTerm("P(A | B)", "posterior"),
- ", the probability that ",
- labels.eventA,
- " after observing that ",
- labels.eventB,
+ "To find ",
+ bayesPhrase(
+ `the probability of the event "${labels.eventA}" occurring given the event "${labels.eventB}" has occurred`,
+ "posterior",
+ ),
+ ", we multiply ",
+ bayesPhrase(
+ `the ${likelihoodPercent} likelihood of the event "${labels.eventB}" given the event "${labels.eventA}"`,
+ "likelihood",
+ ),
+ " by ",
+ bayesPhrase(`the ${priorPercent} prior probability of the event "${labels.eventA}"`, "prior"),
+ " and divide by ",
+ bayesPhrase(
+ `the ${evidencePercent} marginal probability of the event "${labels.eventB}"`,
+ "evidence",
+ ),
+ ". This gives ",
+ bayesPhrase(posteriorPercent, "posterior"),
".",
)
- prior.setDefinition(bayesTerm("P(A)", "prior"), ", the prior probability that ", labels.eventA)
+ prior.setDefinition(
+ bayesTerm("P(A)", "prior"),
+ ", ",
+ bayesPhrase(`the prior probability that ${labels.eventA}`, "prior"),
+ )
likelihood.setDefinition(
bayesTerm("P(B | A)", "likelihood"),
- ", the likelihood that ",
- labels.eventB,
- " given that ",
- labels.eventA,
+ ", ",
+ bayesPhrase(`the likelihood that ${labels.eventB} given that ${labels.eventA}`, "likelihood"),
)
falsePositive.setDefinition(
bayesTerm("P(B | not A)", "neutral"),
- ", the probability that ",
- labels.eventB,
- " given that ",
- notEventA,
+ ", ",
+ bayesPhrase(`the probability that ${labels.eventB} given that ${notEventA}`, "neutral"),
)
evidence.setDefinition(
bayesTerm("P(B)", "evidence"),
- ", the marginal probability that ",
- labels.eventB,
+ ", ",
+ bayesPhrase(`the marginal probability that ${labels.eventB}`, "evidence"),
)
if (getMode() === "computed-evidence") {
@@ -252,7 +239,8 @@ function renderBayesSolver() {
modeText,
"compute ",
bayesTerm("P(B)", "evidence"),
- " from the false positive rate",
+ " from the ",
+ bayesPhrase("false positive rate", "neutral"),
)
} else {
setInlineContent(modeText, "use a known marginal probability ", bayesTerm("P(B)", "evidence"))
@@ -260,29 +248,31 @@ function renderBayesSolver() {
posterior.setDefinition(
bayesTerm("P(A | B)", "posterior"),
- ", the probability that ",
- labels.eventA,
- " given that ",
- labels.eventB,
+ ", ",
+ bayesPhrase(`the probability that ${labels.eventA} given that ${labels.eventB}`, "posterior"),
)
numerator.setDefinition(
bayesTerm("P(B | A)", "likelihood"),
" times ",
bayesTerm("P(A)", "prior"),
- ", likelihood times prior",
+ ", ",
+ bayesPhrase("likelihood times prior", "numerator"),
)
evidenceOutput.setDefinition(
bayesTerm("P(B)", "evidence"),
- ", the marginal probability that ",
- labels.eventB,
+ ", ",
+ bayesPhrase(`the marginal probability that ${labels.eventB}`, "evidence"),
+ )
+ odds.setDefinition(
+ bayesPhrase("posterior odds", "posterior"),
+ " from ",
+ bayesTerm("P(A | B)", "posterior"),
)
- odds.setDefinition("posterior odds from ", bayesTerm("P(A | B)", "posterior"))
}
function update() {
const mode = getMode()
const labels = getEventLabels()
- updateCopy(labels)
const result = solve_bayes_percent(
Number.parseFloat(prior.input.value),
@@ -307,20 +297,21 @@ function renderBayesSolver() {
setResult(numerator.value, numeratorValue)
setResult(evidenceOutput.value, evidenceValue)
+
const formulaValues = {
prior: clampProbability(Number.parseFloat(prior.input.value) / 100),
likelihood: clampProbability(Number.parseFloat(likelihood.input.value) / 100),
evidence: evidenceValue,
numerator: numeratorValue,
}
- const latexEquation = buildBayesLatex(formulaValues)
- latex.output.value = latexEquation.source
- latex.rendered.replaceChildren(createBayesMath(formulaValues))
- latex.status.textContent = ""
+
+ updateCopy(labels, formulaValues)
+ updateSolution(solution, formulaValues)
if (errorCode === 1) {
posterior.value.textContent = "-"
odds.value.textContent = "-"
+ solution.equation.textContent = "-"
error.textContent = `The marginal probability that ${labels.eventB} must be greater than 0.`
solverRoot.classList.add("has-error")
return
@@ -345,14 +336,6 @@ function renderBayesSolver() {
form.addEventListener("input", update)
form.addEventListener("submit", (event) => event.preventDefault())
modeToggle.addEventListener("change", update)
- latex.copyButton.addEventListener("click", async () => {
- try {
- await navigator.clipboard.writeText(latex.output.value)
- latex.status.textContent = "copied"
- } catch {
- latex.status.textContent = "copy failed"
- }
- })
update()
}
@@ -361,82 +344,47 @@ function eventName(value: string, fallback: string) {
return trimmed.length > 0 ? trimmed : fallback
}
-function percentInputText(input: HTMLInputElement) {
- const value = Number.parseFloat(input.value)
- return Number.isFinite(value) ? `${percentFormatter.format(value)}%` : "an unknown percentage"
-}
-
function clampProbability(value: number) {
if (!Number.isFinite(value)) return 0
return Math.min(1, Math.max(0, value))
}
-function latexColor(role: keyof typeof bayesColors, value: string) {
- return `\\textcolor{${bayesColors[role]}}{${value}}`
+function formatProbabilityPercent(value: number) {
+ return Number.isFinite(value) ? `${probabilityToPercent(value)}%` : "an unknown percentage"
}
-function latexNumber(value: number) {
- if (!Number.isFinite(value)) return "\\text{undefined}"
- const rounded = value.toFixed(6).replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "")
- return rounded === "-0" ? "0" : rounded
+function updateSolution(
+ solution: ReturnType,
+ values: BayesFormulaValues,
+) {
+ solution.equation.replaceChildren(createBayesEquation(values))
}
-function buildBayesLatex(values: BayesFormulaValues) {
- const posterior = values.evidence > 0 ? values.numerator / values.evidence : Number.NaN
- const body = [
- "\\begin{aligned}",
- `${latexColor("posterior", "P(A \\mid B)")}`,
- `&= \\frac{${latexColor("likelihood", "P(B \\mid A)")}\\,${latexColor("prior", "P(A)")}}{${latexColor("evidence", "P(B)")}} \\\\`,
- `&= \\frac{${latexColor("likelihood", latexNumber(values.likelihood))}\\cdot` +
- `${latexColor("prior", latexNumber(values.prior))}}` +
- `{${latexColor("evidence", latexNumber(values.evidence))}} \\\\`,
- `&= ${latexColor("posterior", latexNumber(posterior))}`,
- "\\end{aligned}",
- ].join("\n")
-
- const source = [
- "\\[",
- body,
- "\\]",
- ].join("\n")
-
- return { body, source }
+function formatPosteriorPercent(values: BayesFormulaValues) {
+ if (values.evidence <= 0) {
+ return "-"
+ }
+
+ return `${probabilityToPercent(values.numerator / values.evidence)}%`
}
-function createBayesMath(values: BayesFormulaValues) {
- const posterior = values.evidence > 0 ? values.numerator / values.evidence : Number.NaN
+function createBayesEquation(values: BayesFormulaValues) {
return mathElement(
"math",
{ class: "bayes-rendered-math", display: "block" },
mathElement(
- "mtable",
+ "mrow",
{},
- mathRow(
- probabilityAGivenB(),
- fraction(multiply(probabilityBGivenA(), probabilityA()), probabilityB()),
+ coloredMath("posterior", mathElement("mtext", {}, formatPosteriorPercent(values))),
+ mathElement("mo", {}, "="),
+ fraction(
+ multiply(mathNumber(values.likelihood, "likelihood"), mathNumber(values.prior, "prior")),
+ mathNumber(values.evidence, "evidence"),
),
- mathRow(
- null,
- fraction(
- multiply(mathNumber(values.likelihood, "likelihood"), mathNumber(values.prior, "prior")),
- mathNumber(values.evidence, "evidence"),
- ),
- ),
- mathRow(null, mathNumber(posterior, "posterior")),
),
)
}
-function mathRow(left: Element | null, right: Element) {
- return mathElement(
- "mtr",
- {},
- mathElement("mtd", {}, left ?? mathElement("mrow")),
- mathElement("mtd", {}, mathElement("mo", {}, "=")),
- mathElement("mtd", {}, right),
- )
-}
-
function fraction(numerator: Element, denominator: Element) {
return mathElement("mfrac", {}, numerator, denominator)
}
@@ -445,57 +393,16 @@ function multiply(left: Element, right: Element) {
return mathElement("mrow", {}, left, mathElement("mo", {}, "\u00b7"), right)
}
-function probabilityAGivenB() {
- return coloredMath(
- "posterior",
- mathElement("mi", {}, "P"),
- mathElement("mo", {}, "("),
- mathElement("mi", {}, "A"),
- mathElement("mo", {}, "\u2223"),
- mathElement("mi", {}, "B"),
- mathElement("mo", {}, ")"),
- )
-}
-
-function probabilityBGivenA() {
- return coloredMath(
- "likelihood",
- mathElement("mi", {}, "P"),
- mathElement("mo", {}, "("),
- mathElement("mi", {}, "B"),
- mathElement("mo", {}, "\u2223"),
- mathElement("mi", {}, "A"),
- mathElement("mo", {}, ")"),
- )
-}
-
-function probabilityA() {
- return coloredMath(
- "prior",
- mathElement("mi", {}, "P"),
- mathElement("mo", {}, "("),
- mathElement("mi", {}, "A"),
- mathElement("mo", {}, ")"),
- )
-}
-
-function probabilityB() {
+function mathNumber(value: number, role: keyof typeof bayesColors) {
return coloredMath(
- "evidence",
- mathElement("mi", {}, "P"),
- mathElement("mo", {}, "("),
- mathElement("mi", {}, "B"),
- mathElement("mo", {}, ")"),
+ role,
+ mathElement(Number.isFinite(value) ? "mn" : "mtext", {}, bayes_number(value)),
)
}
-function mathNumber(value: number, role: keyof typeof bayesColors) {
- return coloredMath(role, mathElement(Number.isFinite(value) ? "mn" : "mtext", {}, latexNumber(value)))
-}
-
function coloredMath(role: keyof typeof bayesColors, ...children: Array) {
const element = mathElement("mrow", { mathcolor: bayesColors[role] }, ...children)
- ;(element as HTMLElement).style.color = bayesColors[role]
+ ; (element as HTMLElement).style.color = bayesColors[role]
return element
}
@@ -514,39 +421,17 @@ function mathElement(
return element
}
-function createLatexOutput() {
+function createSolutionOutput() {
const element = document.createElement("section")
- element.className = "bayes-latex"
-
- const heading = document.createElement("div")
- heading.className = "bayes-latex-heading"
-
- const label = document.createElement("span")
- label.textContent = "LaTeX"
-
- const copyButton = document.createElement("button")
- copyButton.type = "button"
- copyButton.textContent = "Copy LaTeX"
+ element.className = "bayes-solution"
+ element.setAttribute("aria-live", "polite")
- const status = document.createElement("span")
- status.className = "bayes-latex-status"
- status.setAttribute("aria-live", "polite")
+ const equation = document.createElement("div")
+ equation.className = "bayes-solution-equation"
+ equation.setAttribute("aria-label", "Bayes theorem equation with current numbers")
- heading.append(label, copyButton, status)
-
- const rendered = document.createElement("div")
- rendered.className = "bayes-latex-rendered"
- rendered.setAttribute("aria-label", "Rendered Bayes theorem equation with current numbers")
-
- const output = document.createElement("textarea")
- output.className = "bayes-latex-output"
- output.readOnly = true
- output.rows = 8
- output.spellcheck = false
- output.setAttribute("aria-label", "Bayes theorem LaTeX with current numbers")
-
- element.append(heading, rendered, output)
- return { element, rendered, output, copyButton, status }
+ element.append(equation)
+ return { element, equation }
}
function setInlineContent(element: HTMLElement, ...content: InlineContent[]) {
@@ -564,19 +449,18 @@ function bayesTerm(text: string, role: BayesColorRole) {
return term
}
-function createEventField(
- symbol: string,
- labelText: string,
- placeholder: string,
- value: string,
-) {
+function bayesPhrase(text: string, role: BayesColorRole) {
+ const phrase = document.createElement("span")
+ phrase.className = "bayes-phrase"
+ phrase.dataset.bayesRole = role
+ phrase.textContent = text
+ return phrase
+}
+
+function createEventField(labelText: string, placeholder: string, value: string) {
const label = document.createElement("label")
label.className = "bayes-event-field"
- const symbolText = document.createElement("span")
- symbolText.className = "bayes-solver-symbol"
- symbolText.textContent = symbol
-
const descriptor = document.createElement("span")
descriptor.className = "bayes-solver-definition-text"
descriptor.textContent = labelText
@@ -587,33 +471,19 @@ function createEventField(
input.value = value
input.autocomplete = "off"
- label.append(symbolText, descriptor, input)
+ label.append(descriptor, input)
return { label, input }
}
-function createPercentField(
- symbol: InlineContent[],
- labelContent: InlineContent[],
- value: string,
- role: BayesColorRole,
-) {
+function createPercentField(labelContent: InlineContent[], value: string, role: BayesColorRole) {
const label = document.createElement("label")
label.className = "bayes-solver-field"
label.dataset.bayesRole = role
- const text = document.createElement("span")
- text.className = "bayes-solver-label"
-
- const notation = document.createElement("span")
- notation.className = "bayes-solver-symbol"
- setInlineContent(notation, ...symbol)
-
const definition = document.createElement("span")
definition.className = "bayes-solver-definition-text"
setInlineContent(definition, ...labelContent)
- text.append(notation, definition)
-
const control = document.createElement("span")
control.className = "bayes-solver-control"
@@ -629,7 +499,7 @@ function createPercentField(
percent.textContent = "%"
control.append(input, percent)
- label.append(text, control)
+ label.append(definition, control)
return {
label,
@@ -640,12 +510,7 @@ function createPercentField(
}
}
-function createOutputItem(
- symbol: InlineContent[],
- labelContent: InlineContent[],
- title: string,
- role: BayesColorRole,
-) {
+function createOutputItem(labelContent: InlineContent[], title: string, role: BayesColorRole) {
const element = document.createElement("article")
element.className = "bayes-solver-result"
element.dataset.bayesRole = role
@@ -653,10 +518,6 @@ function createOutputItem(
const label = document.createElement("span")
label.className = "bayes-solver-label"
- const notation = document.createElement("span")
- notation.className = "bayes-solver-symbol"
- setInlineContent(notation, ...symbol)
-
const definition = document.createElement("span")
definition.className = "bayes-solver-definition-text"
setInlineContent(definition, ...labelContent)
@@ -664,7 +525,7 @@ function createOutputItem(
const value = document.createElement("strong")
value.title = title
- label.append(notation, definition)
+ label.append(definition)
element.append(label, value)
return {
element,
diff --git a/ts/src/glsl/index.ts b/ts/src/glsl/index.ts
index 8a189b1b..fa861ac6 100644
--- a/ts/src/glsl/index.ts
+++ b/ts/src/glsl/index.ts
@@ -21,8 +21,6 @@ convertButton.addEventListener("click", () => {
downloadButton.addEventListener("click", () => {
if (shader && extract && raymarch) {
- const arr = shader.value.split("/").filter((x) => x.length > 0)
-
const xhttp = new XMLHttpRequest()
xhttp.onload = function () {
if (this.responseText) {
@@ -30,7 +28,7 @@ downloadButton.addEventListener("click", () => {
}
}
- const shaderId = arr[arr.length - 1]
+ const shaderId = wasm.shader_id_from_url(shader.value)
if (shaderId) {
xhttp.open("GET", `https://www.shadertoy.com/api/v1/shaders/${shaderId}?key=NtHtMm`)
xhttp.send()
@@ -112,7 +110,7 @@ shader.addEventListener("input", () => {
inp.value = JSON.parse(this.responseText).Shader.renderpass[0].code
}
}
- const shaderId = shader.value.split("/").pop()
+ const shaderId = wasm.shader_id_from_url(shader.value)
xhttp.open("GET", `https://www.shadertoy.com/api/v1/shaders/${shaderId}?key=NtHtMm`)
xhttp.send()
})
diff --git a/ts/src/movies.ts b/ts/src/movies.ts
index 16e133bf..baa452e4 100644
--- a/ts/src/movies.ts
+++ b/ts/src/movies.ts
@@ -1,3 +1,5 @@
+import { movie_candidate_is_visible, random_index } from "wasm"
+
const timeout = 0.25
function getRandomMovie(id: string, button: HTMLButtonElement): void {
@@ -23,7 +25,7 @@ function getRandomMovie(id: string, button: HTMLButtonElement): void {
wheel.spin()
setTimeout(() => {
- const randomMovie = moviesToWatch[Math.floor(Math.random() * moviesToWatch.length)]
+ const randomMovie = moviesToWatch[random_index(moviesToWatch.length)]
const result = document.createElement("h2")
result.id = "result"
result.textContent = "random movie: "
@@ -62,10 +64,7 @@ function collectMovies(id: string) {
const headingContent: string = (heading.nextElementSibling?.nextElementSibling as HTMLElement)
.innerText
- const moviesToWatch = headingContent
- .split("\n")
- .filter((x) => !x.includes("BREAK"))
- .filter((x) => !/\(\d{4}-\d{2}-\d{2}\)$/.test(x))
+ const moviesToWatch = headingContent.split("\n").filter((x) => movie_candidate_is_visible(x))
return moviesToWatch
}
diff --git a/ts/src/photography.ts b/ts/src/photography.ts
new file mode 100644
index 00000000..34b8a264
--- /dev/null
+++ b/ts/src/photography.ts
@@ -0,0 +1,130 @@
+import { photo_caption, photo_count_label, photo_manifest_entry_is_valid } from "wasm"
+
+interface Photo {
+ src: string
+ title: string
+ meta?: string
+}
+
+const fallbackPhotos: Photo[] = [
+ {
+ src: "/misc/media/PXL_20241016_102737105.RESTORED.jpg",
+ title: "weathered sign",
+ meta: "starter image from the existing media folder",
+ },
+]
+
+const gallery = document.getElementById("photo-gallery")
+const count = document.getElementById("photo-gallery-count")
+const dialog = document.getElementById("photo-lightbox") as HTMLDialogElement | null
+const dialogImage = dialog?.querySelector("img") ?? null
+const dialogCaption = dialog?.querySelector("figcaption") ?? null
+const closeButton = dialog?.querySelector(".photo-lightbox-close") ?? null
+const prevButton = dialog?.querySelector(".photo-lightbox-prev") ?? null
+const nextButton = dialog?.querySelector(".photo-lightbox-next") ?? null
+
+let photos = fallbackPhotos
+let currentIndex = 0
+
+function isPhoto(value: unknown): value is Photo {
+ if (!value || typeof value !== "object") {
+ return false
+ }
+
+ const candidate = value as Partial
+ return (
+ typeof candidate.src === "string" &&
+ typeof candidate.title === "string" &&
+ photo_manifest_entry_is_valid(candidate.src, candidate.title)
+ )
+}
+
+async function loadPhotos(): Promise {
+ try {
+ const response = await fetch("/photography/gallery.json", { cache: "no-cache" })
+ if (response.ok) {
+ const loaded: unknown = await response.json()
+ if (Array.isArray(loaded) && loaded.length > 0 && loaded.every(isPhoto)) {
+ photos = loaded
+ }
+ }
+ } catch {
+ photos = fallbackPhotos
+ }
+
+ renderGallery()
+}
+
+function renderGallery(): void {
+ gallery?.replaceChildren()
+
+ for (const [index, photo] of photos.entries()) {
+ const card = document.createElement("a")
+ card.className = "photo-card"
+ card.href = photo.src
+ card.dataset.index = String(index)
+
+ const image = document.createElement("img")
+ image.src = photo.src
+ image.alt = photo.title
+ image.loading = "lazy"
+
+ const text = document.createElement("span")
+ const title = document.createElement("strong")
+ title.textContent = photo.title
+ const meta = document.createElement("small")
+ meta.textContent = photo.meta ?? ""
+ text.append(title, meta)
+
+ card.append(image, text)
+ card.addEventListener("click", (event) => {
+ event.preventDefault()
+ showPhoto(index)
+ })
+ gallery?.append(card)
+ }
+
+ if (count) {
+ count.textContent = photo_count_label(photos.length)
+ }
+}
+
+function showPhoto(index: number): void {
+ if (!dialog || !dialogImage || !dialogCaption || photos.length === 0) {
+ return
+ }
+
+ currentIndex = (index + photos.length) % photos.length
+ const photo = photos[currentIndex]
+ dialogImage.src = photo.src
+ dialogImage.alt = photo.title
+ dialogCaption.textContent = photo_caption(photo.title, photo.meta ?? "")
+ dialog.showModal()
+}
+
+function stepPhoto(delta: number): void {
+ showPhoto(currentIndex + delta)
+}
+
+closeButton?.addEventListener("click", () => dialog?.close())
+prevButton?.addEventListener("click", () => stepPhoto(-1))
+nextButton?.addEventListener("click", () => stepPhoto(1))
+dialog?.addEventListener("click", (event) => {
+ if (event.target === dialog) {
+ dialog.close()
+ }
+})
+window.addEventListener("keydown", (event) => {
+ if (!dialog?.open) {
+ return
+ }
+ if (event.key === "ArrowLeft") {
+ stepPhoto(-1)
+ } else if (event.key === "ArrowRight") {
+ stepPhoto(1)
+ } else if (event.key === "Escape") {
+ dialog.close()
+ }
+})
+
+void loadPhotos()
diff --git a/ts/src/polyrhythm.ts b/ts/src/polyrhythm.ts
index 28288e68..bf9fb0a4 100644
--- a/ts/src/polyrhythm.ts
+++ b/ts/src/polyrhythm.ts
@@ -1,79 +1,131 @@
-import("./polyrhythm/index.js").catch((e) => console.error("Error importing `.js`:", e))
+import("./polyrhythm/index.js").catch((error) =>
+ console.error("Error importing polyrhythm:", error),
+)
+
+type AudioContextConstructor = typeof AudioContext
+type BrowserWindow = Window & {
+ webkitAudioContext?: AudioContextConstructor
+}
+
+type PendingHit = {
+ frequency: number
+ duration: number
+}
+
declare global {
interface Window {
play_beep: (frequency: number, duration: number) => void
+ stop_audio: () => void
}
}
-window.play_beep = play_beep
+let audioContext: AudioContext | null = null
+let masterGain: GainNode | null = null
+let compressor: DynamicsCompressorNode | null = null
+let pendingHits: PendingHit[] = []
+let flushScheduled = false
-function play_beep(frequency: number, duration: number) {
- const context = new (window.AudioContext || window.AudioContext)()
- const oscillator = context.createOscillator()
- const gainNode = context.createGain()
+window.play_beep = playBeep
+window.stop_audio = stopAudio
- oscillator.frequency.value = frequency
- oscillator.connect(gainNode)
- gainNode.connect(context.destination)
+function playBeep(frequency: number, duration: number) {
+ if (!Number.isFinite(frequency) || frequency <= 0) {
+ return
+ }
- oscillator.start()
- gainNode.gain.exponentialRampToValueAtTime(0.00001, context.currentTime + duration)
- oscillator.stop(context.currentTime + duration)
-}
+ pendingHits.push({
+ frequency,
+ duration: Number.isFinite(duration) && duration > 0 ? duration : 0.09,
+ })
-function updateHash() {
- const baseElement = document.getElementById("base")
- const tempoElement = document.getElementById("tempo")
- const subdivisionsElement = document.getElementById("subdivisions")
- const pitchElement = document.getElementById("pitch")
-
- const base = baseElement ? (baseElement as HTMLInputElement).value : ""
- const tempo = tempoElement ? (tempoElement as HTMLInputElement).value : ""
- const subdivisions = subdivisionsElement ? (subdivisionsElement as HTMLInputElement).value : ""
- const pitch = pitchElement ? (pitchElement as HTMLInputElement).value : ""
- const hashStr = `base=${encodeURIComponent(base)}&tempo=${encodeURIComponent(tempo)}&subdivisions=${encodeURIComponent(subdivisions)}&pitch=${encodeURIComponent(pitch)}`
- window.location.hash = hashStr
+ if (!flushScheduled) {
+ flushScheduled = true
+ queueMicrotask(flushPendingHits)
+ }
}
-// Load settings from URL hash if available
-function loadFromHash() {
- const hash = window.location.hash.substring(1) // remove the '#' character
- if (!hash) return
- hash.split("&").forEach((param) => {
- const [key, value] = param.split("=")
- if (key && value) {
- const elem = document.getElementById(key)
- if (elem) {
- ;(elem as HTMLInputElement).value = decodeURIComponent(value)
- }
- }
- })
+function stopAudio() {
+ pendingHits = []
+ flushScheduled = false
+ if (audioContext) {
+ void audioContext.close()
+ }
+ audioContext = null
+ masterGain = null
+ compressor = null
}
-function startPolyrhythm() {
- console.log("Polyrhythm started with settings:", {
- base: (document.getElementById("base") as HTMLInputElement)?.value || "",
- tempo: (document.getElementById("tempo") as HTMLInputElement)?.value,
- subdivisions: (document.getElementById("subdivisions") as HTMLInputElement)?.value,
- pitch: (document.getElementById("pitch") as HTMLInputElement)?.value,
- })
+function flushPendingHits() {
+ flushScheduled = false
+ const hits = pendingHits
+ pendingHits = []
+ if (hits.length === 0) {
+ return
+ }
+
+ void playHitGroup(hits)
}
-function stopPolyrhythm() {
- console.log("Polyrhythm stopped.")
+async function playHitGroup(hits: PendingHit[]) {
+ const context = getAudioContext()
+ if (!context) {
+ return
+ }
+
+ if (context.state === "suspended") {
+ await context.resume()
+ }
+
+ const startTime = context.currentTime + 0.004
+ const duration = Math.max(...hits.map((hit) => hit.duration))
+ const voicePeak = Math.min(0.14, 0.22 / Math.sqrt(hits.length))
+ const groupDuration = Math.max(0.055, Math.min(0.16, duration))
+
+ for (const hit of hits) {
+ const oscillator = context.createOscillator()
+ const gain = context.createGain()
+ oscillator.type = hits.length > 1 ? "sine" : "triangle"
+ oscillator.frequency.setValueAtTime(hit.frequency, startTime)
+
+ gain.gain.setValueAtTime(0.0001, startTime)
+ gain.gain.linearRampToValueAtTime(voicePeak, startTime + 0.006)
+ gain.gain.exponentialRampToValueAtTime(0.0001, startTime + groupDuration)
+
+ oscillator.connect(gain)
+ gain.connect(masterGain!)
+
+ oscillator.start(startTime)
+ oscillator.stop(startTime + groupDuration + 0.02)
+ oscillator.addEventListener("ended", () => {
+ oscillator.disconnect()
+ gain.disconnect()
+ })
+ }
}
-// Attach event listeners
-document.getElementById("start-button")?.addEventListener("click", () => {
- startPolyrhythm()
- updateHash()
-})
+function getAudioContext() {
+ if (audioContext && masterGain && compressor) {
+ return audioContext
+ }
-document.getElementById("stop-button")?.addEventListener("click", () => {
- stopPolyrhythm()
-})
+ const AudioCtor = window.AudioContext ?? (window as BrowserWindow).webkitAudioContext
+ if (!AudioCtor) {
+ return null
+ }
-// On page load, apply settings from the URL hash
-window.addEventListener("load", loadFromHash)
+ audioContext = new AudioCtor()
+ masterGain = audioContext.createGain()
+ compressor = audioContext.createDynamicsCompressor()
-console.log("test")
+ masterGain.gain.value = 0.82
+ compressor.threshold.value = -18
+ compressor.knee.value = 18
+ compressor.ratio.value = 6
+ compressor.attack.value = 0.004
+ compressor.release.value = 0.12
+
+ masterGain.connect(compressor)
+ compressor.connect(audioContext.destination)
+
+ return audioContext
+}
diff --git a/ts/src/polyrhythm/index.ts b/ts/src/polyrhythm/index.ts
index a6487ed9..c9f83c75 100644
--- a/ts/src/polyrhythm/index.ts
+++ b/ts/src/polyrhythm/index.ts
@@ -1,23 +1,152 @@
import { main, start_with_settings, stop } from "wasm"
+type PolyrhythmSettings = {
+ base: number
+ tempo: number
+ subdivisions: string
+ pitch: number
+}
+
main()
-const startButton = document.getElementById("start-button") as HTMLButtonElement
-startButton.addEventListener("click", () => {
- const baseInput = document.getElementById("base") as HTMLInputElement
- const tempoInput = document.getElementById("tempo") as HTMLInputElement
- const subdivisionsInput = document.getElementById("subdivisions") as HTMLInputElement
- const pitchInput = document.getElementById("pitch") as HTMLInputElement
+const baseInput = requiredInput("base")
+const tempoInput = requiredInput("tempo")
+const subdivisionsInput = requiredInput("subdivisions")
+const pitchInput = requiredInput("pitch")
+const startButton = requiredButton("start-button")
+const stopButton = requiredButton("stop-button")
+const statusElement = document.getElementById("polyrhythm-status")
+const presetButtons = document.querySelectorAll("[data-polyrhythm-preset]")
- const base = parseInt(baseInput.value) || 4
- const tempo = parseInt(tempoInput.value) || 120
- const subdivisions = subdivisionsInput.value || "3:4"
- const pitch = parseInt(pitchInput.value) || 440
+applySettings(readSettingsFromHash() ?? readSettingsFromControls())
+setRunning(false)
- start_with_settings(base, tempo, subdivisions, pitch)
+startButton.addEventListener("click", () => {
+ const settings = readSettingsFromControls()
+ applySettings(settings)
+ updateHash(settings)
+
+ try {
+ start_with_settings(settings.base, settings.tempo, settings.subdivisions, settings.pitch)
+ setRunning(true)
+ setStatus(`${settings.subdivisions.replaceAll(":", " against ")} at ${settings.tempo} BPM`)
+ } catch (error) {
+ setRunning(false)
+ setStatus(error instanceof Error ? error.message : "Could not start polyrhythm")
+ }
})
-const stopButton = document.getElementById("stop-button") as HTMLButtonElement
stopButton.addEventListener("click", () => {
stop()
+ window.stop_audio?.()
+ setRunning(false)
+ setStatus("Stopped")
+})
+
+presetButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ const preset = button.dataset.polyrhythmPreset
+ if (!preset) {
+ return
+ }
+
+ subdivisionsInput.value = preset
+ const settings = readSettingsFromControls()
+ applySettings(settings)
+ updateHash(settings)
+ setStatus(`${settings.subdivisions.replaceAll(":", " against ")} ready`)
+ })
})
+
+for (const input of [baseInput, tempoInput, subdivisionsInput, pitchInput]) {
+ input.addEventListener("change", () => {
+ const settings = readSettingsFromControls()
+ applySettings(settings)
+ updateHash(settings)
+ })
+}
+
+function requiredInput(id: string) {
+ const input = document.getElementById(id)
+ if (!(input instanceof HTMLInputElement)) {
+ throw new Error(`Missing input #${id}`)
+ }
+ return input
+}
+
+function requiredButton(id: string) {
+ const button = document.getElementById(id)
+ if (!(button instanceof HTMLButtonElement)) {
+ throw new Error(`Missing button #${id}`)
+ }
+ return button
+}
+
+function readSettingsFromControls(): PolyrhythmSettings {
+ return {
+ base: readInteger(baseInput.value, 4, 1, 16),
+ tempo: readInteger(tempoInput.value, 120, 20, 280),
+ subdivisions: normalizeSubdivisions(subdivisionsInput.value),
+ pitch: readInteger(pitchInput.value, 440, 80, 1400),
+ }
+}
+
+function readSettingsFromHash(): PolyrhythmSettings | null {
+ const hash = window.location.hash.slice(1)
+ if (!hash) {
+ return null
+ }
+
+ const params = new URLSearchParams(hash)
+ return {
+ base: readInteger(params.get("base"), 4, 1, 16),
+ tempo: readInteger(params.get("tempo"), 120, 20, 280),
+ subdivisions: normalizeSubdivisions(params.get("subdivisions") ?? "3:4"),
+ pitch: readInteger(params.get("pitch"), 440, 80, 1400),
+ }
+}
+
+function applySettings(settings: PolyrhythmSettings) {
+ baseInput.value = String(settings.base)
+ tempoInput.value = String(settings.tempo)
+ subdivisionsInput.value = settings.subdivisions
+ pitchInput.value = String(settings.pitch)
+}
+
+function updateHash(settings: PolyrhythmSettings) {
+ const params = new URLSearchParams({
+ base: String(settings.base),
+ tempo: String(settings.tempo),
+ subdivisions: settings.subdivisions,
+ pitch: String(settings.pitch),
+ })
+ history.replaceState(null, "", `#${params}`)
+}
+
+function readInteger(value: string | null, fallback: number, min: number, max: number) {
+ const parsed = Number.parseInt(value ?? "", 10)
+ if (!Number.isFinite(parsed)) {
+ return fallback
+ }
+ return Math.min(max, Math.max(min, parsed))
+}
+
+function normalizeSubdivisions(value: string) {
+ const parts = value
+ .split(/[,: ]+/)
+ .map((part) => readInteger(part, 0, 0, 32))
+ .filter((part) => part > 0)
+
+ return parts.length > 0 ? parts.join(":") : "3:4"
+}
+
+function setRunning(isRunning: boolean) {
+ startButton.disabled = isRunning
+ stopButton.disabled = !isRunning
+}
+
+function setStatus(message: string) {
+ if (statusElement) {
+ statusElement.textContent = message
+ }
+}
diff --git a/ts/src/textprocessing/index.ts b/ts/src/textprocessing/index.ts
index bfd8dc0e..d8797e66 100644
--- a/ts/src/textprocessing/index.ts
+++ b/ts/src/textprocessing/index.ts
@@ -73,8 +73,8 @@ wasm.main()
const wasmTransform =
(index: number, leftToRight: boolean): Transform =>
- (text) =>
- wasm.transform_text(index, leftToRight, text)
+ (text) =>
+ wasm.transform_text(index, leftToRight, text)
const transforms: TransformDefinition[] = [
{
@@ -596,7 +596,15 @@ function renderGraphWorkspace(): HTMLElement {
status.className = "graph-status"
status.setAttribute("role", "status")
- controls.append(addSelect, addButton, addSourceButton, addOutputButton, arrangeButton, resetButton, status)
+ controls.append(
+ addSelect,
+ addButton,
+ addSourceButton,
+ addOutputButton,
+ arrangeButton,
+ resetButton,
+ status,
+ )
header.append(title, controls)
const board = document.createElement("div")
@@ -684,8 +692,7 @@ function renderGraphWorkspace(): HTMLElement {
}
function getNodeTitle(node: GraphNode): string {
- const base =
- node.kind === "source" ? "Source" : node.kind === "output" ? "Output" : "Transform"
+ const base = node.kind === "source" ? "Source" : node.kind === "output" ? "Output" : "Transform"
const suffix = new RegExp(`^${node.kind}-(\\d+)$`).exec(node.id)?.[1]
return suffix ? `${base} ${suffix}` : base
}
@@ -861,7 +868,9 @@ function renderGraphWorkspace(): HTMLElement {
connections: state.connections,
} satisfies StoredGraph),
)
- } catch { }
+ } catch {
+ // Ignore storage failures in private browsing or quota-limited contexts.
+ }
}
function clearGraph() {
@@ -1642,8 +1651,9 @@ function renderTransform(definition: TransformDefinition): RenderedTransform {
title.className = "transform-title"
const titleText = document.createElement("span")
- titleText.textContent = `${definition.leftLabel} ${definition.rightToLeft ? "↔" : "→"} ${definition.rightLabel
- }`
+ titleText.textContent = `${definition.leftLabel} ${definition.rightToLeft ? "↔" : "→"} ${
+ definition.rightLabel
+ }`
const direction = document.createElement("span")
direction.className = "transform-direction"
diff --git a/ts/src/therenderingequation.ts b/ts/src/therenderingequation.ts
index 7131532f..72a706d5 100644
--- a/ts/src/therenderingequation.ts
+++ b/ts/src/therenderingequation.ts
@@ -1,3 +1,5 @@
+import { random_range } from "wasm"
+
document.addEventListener("DOMContentLoaded", () => {
const elements: NodeListOf = document.querySelectorAll(
"#interactiveSvg path, #interactiveSvg rect",
@@ -17,8 +19,8 @@ document.addEventListener("DOMContentLoaded", () => {
return
}
- const frequency = Math.random() * 3 + 2 // 2-5 seconds duration
- const amplitude = Math.random() * 5 + 8 // 5-10 pixels vertical movement
+ const frequency = random_range(2, 5)
+ const amplitude = random_range(8, 13)
element.style.animation = `moveUpDown ${frequency}s ease-in-out infinite alternate`
element.style.setProperty("--amplitude", `${amplitude}px`)
diff --git a/ts/src/trolley.ts b/ts/src/trolley.ts
index 03595d3b..8a2b7eb3 100644
--- a/ts/src/trolley.ts
+++ b/ts/src/trolley.ts
@@ -1,13 +1,11 @@
+import { trolley_media_src, trolley_random_index } from "wasm"
+
const NUM = 63
document.addEventListener("DOMContentLoaded", async () => {
- const trolleyPath = "/misc/trolley/"
- const extensions = ["mp4", "jpg"]
- const randomNumber = Math.floor(Math.random() * NUM) + 1
-
- const randomNum = String(randomNumber).padStart(2, "0")
-
- const mp4Src = `${trolleyPath}${randomNum}.${extensions[0]}`
- const jpgSrc = `${trolleyPath}${randomNum}.${extensions[1]}`
+ const trolleyPath = "/misc/trolley"
+ const randomNumber = trolley_random_index(NUM)
+ const mp4Src = trolley_media_src(trolleyPath, randomNumber, "mp4")
+ const jpgSrc = trolley_media_src(trolleyPath, randomNumber, "jpg")
const response = await fetch(jpgSrc)
const selectedSrc = response.ok ? jpgSrc : mp4Src
diff --git a/ts/src/tuningplayground/UI.ts b/ts/src/tuningplayground/UI.ts
index 56185a4d..f99d0fa4 100644
--- a/ts/src/tuningplayground/UI.ts
+++ b/ts/src/tuningplayground/UI.ts
@@ -25,6 +25,7 @@ export const tuningSelect = document.getElementById("tuningSelect") as HTMLSelec
export const volumeSlider = document.getElementById("volumeSlider") as HTMLInputElement
const transpose = document.getElementById("transpose") as HTMLInputElement
+const DEFAULT_TRANSPOSE = 24
export const output = document.getElementById("output") as HTMLElement
const chordInput = document.getElementById("chordInput") as HTMLInputElement
@@ -61,9 +62,15 @@ shareMarked.onclick = sharedMarkedKeys
nameChord.onclick = updateChordName
clearChord.onclick = clearChordInput
-export let tranposeValue = 0
+export let tranposeValue = readTransposeValue()
function transposeChange(): void {
- tranposeValue = parseInt(transpose.value)
+ tranposeValue = readTransposeValue()
+ transpose.value = tranposeValue.toString()
+}
+
+function readTransposeValue(): number {
+ const parsed = Number.parseInt(transpose.value, 10)
+ return Number.isFinite(parsed) ? parsed : DEFAULT_TRANSPOSE
}
export let volumeValue = 0.25
@@ -88,7 +95,10 @@ function initOrGetMidiFile(): Promise {
return midiFile
})
.catch((error) => {
- setTuningPlaygroundStatus(`Could not load default MIDI file: ${formatError(error)}`, "error")
+ setTuningPlaygroundStatus(
+ `Could not load default MIDI file: ${formatError(error)}`,
+ "error",
+ )
throw error
})
}
@@ -250,8 +260,7 @@ function createAndCopyUrl(keys: number[]): () => void {
}
function generateHash(keys: number[]) {
- const hash = keys.join(",")
- return hash
+ return wasm.tuning_marked_hash(keys.join(","))
}
export function logToDiv(message: string, notes: number[]): void {
@@ -373,11 +382,11 @@ function applySharedTuningFromUrl(): void {
}
if (octave) {
- octaveSize.value = positiveInteger(octave, 12).toString()
+ octaveSize.value = wasm.tuning_positive_integer(octave, 12).toString()
}
if (step) {
- stepSize.value = positiveInteger(step, 7).toString()
+ stepSize.value = wasm.tuning_positive_integer(step, 7).toString()
}
}
@@ -386,15 +395,11 @@ function tuningOptionExists(value: string): boolean {
}
function positiveInteger(value: string, fallback: number): number {
- const parsed = Number.parseInt(value, 10)
- if (Number.isFinite(parsed) && parsed > 0) {
- return parsed
- }
- return fallback
+ return wasm.tuning_positive_integer(value, fallback)
}
function currentMarkedHash(): string {
- return markedKeys.length > 0 ? generateHash(markedKeys) : window.location.hash.substring(1)
+ return wasm.tuning_hash_or_fallback(markedKeys.join(","), window.location.hash)
}
function tuningUrl(hash = currentMarkedHash()): string {
diff --git a/ts/webpack.config.mjs b/ts/webpack.config.mjs
index bfc055c3..1651e71d 100644
--- a/ts/webpack.config.mjs
+++ b/ts/webpack.config.mjs
@@ -27,6 +27,7 @@ const config = {
chars: "./dist/chars.js",
pokemon: "./dist/pokemon.js",
bayestheorem: "./dist/bayestheorem.js",
+ photography: "./dist/photography.js",
},
output: {
path: path.resolve(contentJsDir),
diff --git a/wasm/adventofcode/.gitignore b/wasm/adventofcode/.gitignore
index 8e281522..2a62ab78 100644
--- a/wasm/adventofcode/.gitignore
+++ b/wasm/adventofcode/.gitignore
@@ -1,7 +1,6 @@
www/
debug/
target/
-Cargo.lock
**/*.rs.bk
*.pdb
-src/*/day*/solution*.html
\ No newline at end of file
+src/*/day*/solution*.html
diff --git a/wasm/adventofcode/Cargo.lock b/wasm/adventofcode/Cargo.lock
new file mode 100644
index 00000000..6d9033c4
--- /dev/null
+++ b/wasm/adventofcode/Cargo.lock
@@ -0,0 +1,1945 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "adventofcode"
+version = "1.0.0"
+dependencies = [
+ "minify-html",
+ "syntect",
+ "walkdir",
+]
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64-simd"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5"
+dependencies = [
+ "simd-abstraction",
+]
+
+[[package]]
+name = "base64-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
+dependencies = [
+ "outref 0.5.2",
+ "vsimd",
+]
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "compact_str"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "const-str"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3"
+dependencies = [
+ "const-str-proc-macro",
+]
+
+[[package]]
+name = "const-str-proc-macro"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "cow-utils"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "cssparser"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf 0.11.3",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-color"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f"
+dependencies = [
+ "cssparser",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dashmap"
+version = "5.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
+dependencies = [
+ "cfg-if",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "data-url"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "dragonbox_ecma"
+version = "0.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d742b56656e8b14d63e7ea9806597b1849ae25412584c8adf78c0f67bd985e66"
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash 0.7.8",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-escape-simd"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35e770254dd7802184595b1d30da2a15cb72569e2aca2b177aef8d22eac8a693"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "lightningcss"
+version = "1.0.0-alpha.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb6314c2f0590ac93c86099b98bb7ba8abcf759bfd89604ffca906472bb54937"
+dependencies = [
+ "ahash 0.8.12",
+ "bitflags",
+ "const-str",
+ "cssparser",
+ "cssparser-color",
+ "dashmap",
+ "data-encoding",
+ "getrandom 0.3.4",
+ "indexmap",
+ "itertools 0.10.5",
+ "lazy_static",
+ "lightningcss-derive",
+ "parcel_selectors",
+ "parcel_sourcemap",
+ "pastey",
+ "pathdiff",
+ "rayon",
+ "serde",
+ "serde-content",
+ "smallvec",
+]
+
+[[package]]
+name = "lightningcss-derive"
+version = "1.0.0-alpha.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "minify-html"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644c1511457e5f70dcbc03c932d4bd26351eceb8657b119c919dde2e65b6d120"
+dependencies = [
+ "ahash 0.8.12",
+ "aho-corasick",
+ "lightningcss",
+ "memchr",
+ "minify-html-common",
+ "once_cell",
+ "oxc_allocator",
+ "oxc_codegen",
+ "oxc_minifier",
+ "oxc_parser",
+ "oxc_span",
+]
+
+[[package]]
+name = "minify-html-common"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c3d71ae301c4e36949937ed9d121e1bf6847f2dc69ec20929b379d14659aac"
+dependencies = [
+ "ahash 0.8.12",
+ "aho-corasick",
+ "itertools 0.14.0",
+ "memchr",
+ "once_cell",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "nonmax"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "onig"
+version = "6.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
+dependencies = [
+ "bitflags",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "outref"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4"
+
+[[package]]
+name = "outref"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
+
+[[package]]
+name = "owo-colors"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
+
+[[package]]
+name = "oxc-browserslist"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb7a1163a5501f935f8722d839b576491b749c695e7a066aa0b8df988b806df"
+dependencies = [
+ "flate2",
+ "postcard",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "oxc-miette"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4356a61f2ed4c9b3610245215fbf48970eb277126919f87db9d0efa93a74245c"
+dependencies = [
+ "cfg-if",
+ "owo-colors",
+ "oxc-miette-derive",
+ "textwrap",
+ "thiserror",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "oxc-miette-derive"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b237422b014f8f8fff75bb9379e697d13f8d57551a22c88bebb39f073c1bf696"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "oxc_allocator"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "433214c659b860685d987ca25a523a544d35ebf87ee3658a942fd1c664cfa49b"
+dependencies = [
+ "allocator-api2",
+ "bumpalo",
+ "hashbrown 0.16.1",
+ "oxc_data_structures",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_ast"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e78ea25d23521092edcd509198635dd0bd96df7fb71349bcd8087bb8f6a615d"
+dependencies = [
+ "bitflags",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_estree",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_ast_macros"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3f29bf83925451a7ebbd30d3ff449414cc230c9b63aba70487c2ca74ea1e3ed"
+dependencies = [
+ "phf 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "oxc_ast_visit"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808862f42c9331954cf187261d6a48dbf471ccbe60b6a35f6b1056c11f32e95b"
+dependencies = [
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_codegen"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "480bab938439c921d34750708abf15aacf253ea11e2f348e4b085cbf697f4ea2"
+dependencies = [
+ "bitflags",
+ "cow-utils",
+ "dragonbox_ecma",
+ "itoa",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_index",
+ "oxc_semantic",
+ "oxc_sourcemap",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_compat"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd06b5cb59ece933be410adc6f811108a10c567fa2d105321f8d0f01cc4ab0c6"
+dependencies = [
+ "cow-utils",
+ "oxc-browserslist",
+ "oxc_syntax",
+ "rustc-hash",
+ "serde",
+]
+
+[[package]]
+name = "oxc_data_structures"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd090988aa69c1e394f424289abb9bb1281448a072419ca556a07228ad36b854"
+
+[[package]]
+name = "oxc_diagnostics"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b8a09558d38ee7f23faf0249cdb37b5b26f53a7375941f8636c41c661b283ce"
+dependencies = [
+ "cow-utils",
+ "oxc-miette",
+ "percent-encoding",
+]
+
+[[package]]
+name = "oxc_ecmascript"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0002ece1cc266aa6654913d39cf552b0cf501afce9d953b87c283dfcd307266"
+dependencies = [
+ "cow-utils",
+ "num-bigint",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_estree"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af85b24b7e10bf0ccb252f16d38492f51236c1ba146f62013993667cbf7fa649"
+
+[[package]]
+name = "oxc_index"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3e6120999627ec9703025eab7c9f410ebb7e95557632a8902ca48210416c2b"
+dependencies = [
+ "nonmax",
+ "serde",
+]
+
+[[package]]
+name = "oxc_mangler"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb751e1971f0809547347edfebe3755d5bed354aacf3ca7fe7216b901a11dd1d"
+dependencies = [
+ "itertools 0.14.0",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_index",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_minifier"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e836caa36befa6f9603eebf56b0c08d1e164ae31160c8e99ab3f535203065a4c"
+dependencies = [
+ "cow-utils",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_codegen",
+ "oxc_compat",
+ "oxc_data_structures",
+ "oxc_ecmascript",
+ "oxc_index",
+ "oxc_mangler",
+ "oxc_parser",
+ "oxc_regular_expression",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_syntax",
+ "oxc_traverse",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_parser"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb730ab93ff23bbc471ef7109f847afa709bb284dd52a5d3366283d724858158"
+dependencies = [
+ "bitflags",
+ "cow-utils",
+ "memchr",
+ "num-bigint",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+ "seq-macro",
+]
+
+[[package]]
+name = "oxc_regular_expression"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27bafc3035e3b0fe555ae56f7bbc108a7ee520b0858250ba16d197e44ca83746"
+dependencies = [
+ "bitflags",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_diagnostics",
+ "oxc_span",
+ "phf 0.13.1",
+ "rustc-hash",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "oxc_semantic"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578e5fa0b975a28e1d1ffa6c81c8df094545fe3c3225956d4e93ffe1ade3dae0"
+dependencies = [
+ "itertools 0.14.0",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_index",
+ "oxc_span",
+ "oxc_syntax",
+ "phf 0.13.1",
+ "rustc-hash",
+ "self_cell",
+]
+
+[[package]]
+name = "oxc_sourcemap"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d378eb8bad20e89d66276aebab51f6a5408571092cac94abdd3eabb773713d6"
+dependencies = [
+ "base64-simd 0.8.0",
+ "json-escape-simd",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "oxc_span"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71243fe1ece27f71a6be5e2ab11d051792eeb578f3b056d7c9bbe82ec8043bf3"
+dependencies = [
+ "compact_str",
+ "oxc-miette",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_estree",
+]
+
+[[package]]
+name = "oxc_syntax"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93c99e555ed497111004a71fb2aa6f8fb90b0d3e2733aceeb035e24701d69634"
+dependencies = [
+ "bitflags",
+ "cow-utils",
+ "dragonbox_ecma",
+ "nonmax",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_data_structures",
+ "oxc_estree",
+ "oxc_index",
+ "oxc_span",
+ "phf 0.13.1",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "oxc_traverse"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68d00264248ef474988ea02bfa44ccacd9a1d8716f0efb33a9cd6f992cf07b7b"
+dependencies = [
+ "itoa",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_data_structures",
+ "oxc_ecmascript",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+]
+
+[[package]]
+name = "parcel_selectors"
+version = "0.28.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196"
+dependencies = [
+ "bitflags",
+ "cssparser",
+ "log",
+ "phf 0.11.3",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "smallvec",
+]
+
+[[package]]
+name = "parcel_sourcemap"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb"
+dependencies = [
+ "base64-simd 0.7.0",
+ "data-url",
+ "rkyv",
+ "serde",
+ "serde_json",
+ "vlq",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros 0.13.1",
+ "phf_shared 0.13.1",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared 0.11.3",
+ "rand",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared 0.13.1",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plist"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
+dependencies = [
+ "base64",
+ "indexmap",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.39.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "self_cell"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-content"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "simd-abstraction"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987"
+dependencies = [
+ "outref 0.1.0",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "smawk"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syntect"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
+dependencies = [
+ "bincode",
+ "flate2",
+ "fnv",
+ "once_cell",
+ "onig",
+ "plist",
+ "regex-syntax",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "thiserror",
+ "walkdir",
+ "yaml-rust",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "textwrap"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "unicode-id-start"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "uuid"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vlq"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
+
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/wasm/glsl2hlsl/.gitignore b/wasm/glsl2hlsl/.gitignore
index f1a9d0f9..d7b40889 100644
--- a/wasm/glsl2hlsl/.gitignore
+++ b/wasm/glsl2hlsl/.gitignore
@@ -1,6 +1,5 @@
www/
debug/
target/
-Cargo.lock
**/*.rs.bk
-*.pdb
\ No newline at end of file
+*.pdb
diff --git a/wasm/glsl2hlsl/Cargo.lock b/wasm/glsl2hlsl/Cargo.lock
new file mode 100644
index 00000000..8a4688e5
--- /dev/null
+++ b/wasm/glsl2hlsl/Cargo.lock
@@ -0,0 +1,140 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "glsl"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "577ac945ce66a4b7004c6e2807f517f94ef027dda89df578abe1fbb979d22f49"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "glsl2hlsl"
+version = "0.1.0"
+dependencies = [
+ "glsl",
+ "md5",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "md5"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/wasm/textprocessing/.gitignore b/wasm/textprocessing/.gitignore
index f1a9d0f9..d7b40889 100644
--- a/wasm/textprocessing/.gitignore
+++ b/wasm/textprocessing/.gitignore
@@ -1,6 +1,5 @@
www/
debug/
target/
-Cargo.lock
**/*.rs.bk
-*.pdb
\ No newline at end of file
+*.pdb
diff --git a/wasm/textprocessing/Cargo.lock b/wasm/textprocessing/Cargo.lock
new file mode 100644
index 00000000..e0034468
--- /dev/null
+++ b/wasm/textprocessing/Cargo.lock
@@ -0,0 +1,628 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "autocfg"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
+dependencies = [
+ "autocfg 1.5.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bpmf_py"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caae14d254ba113dec824a123282f4ef15077157c4c26517c80939cc415ee387"
+dependencies = [
+ "once_cell",
+ "sugars",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "character_converter"
+version = "2.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f399d4f1a0caebb329b79ea33c81afd6073e603de2589878a0770a9cef837797"
+dependencies = [
+ "bincode",
+ "fst",
+ "once_cell",
+]
+
+[[package]]
+name = "chinese-number"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e964125508474a83c95eb935697abbeb446ff4e9d62c71ce880e3986d1c606b"
+dependencies = [
+ "chinese-variant",
+ "enum-ordinalize",
+ "num-bigint",
+ "num-traits",
+]
+
+[[package]]
+name = "chinese-variant"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58b52a9840ffff5d4d0058ae529fa066a75e794e3125546acfc61c23ad755e49"
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "fst"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a"
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "hangeul_conversion"
+version = "0.1.0"
+dependencies = [
+ "itertools",
+]
+
+[[package]]
+name = "hanja"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f231038f6d37b44fcb898f0348ff34d8b565fbf9ddec9e336c7019723679dde4"
+dependencies = [
+ "phf 0.7.24",
+ "phf_codegen 0.7.24",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "japanese"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07694cc27451ccc3b98784ba4f16c6efc4503465b32f9336c2dff722a22d9db8"
+dependencies = [
+ "maplit",
+ "once_cell",
+]
+
+[[package]]
+name = "japanese_number_converter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e35ddfada703d296a21f6a843cbb64ccda3a0ac154b05f4f3eee6e0a31db175"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg 1.5.0",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "phf"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
+dependencies = [
+ "phf_shared 0.7.24",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e"
+dependencies = [
+ "phf_generator 0.7.24",
+ "phf_shared 0.7.24",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662"
+dependencies = [
+ "phf_shared 0.7.24",
+ "rand 0.6.5",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared 0.11.3",
+ "rand 0.8.6",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
+dependencies = [
+ "siphasher 0.2.3",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher 1.0.2",
+]
+
+[[package]]
+name = "pinyin"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e225f595052d9c46045755be4b8d7950b6d9f3c33e0c0b74ba58f11bbfa8c64b"
+
+[[package]]
+name = "pinyin_zhuyin"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "002d90054842a688ae09569adbf07c9e4868ec19a8b44700299867a28cd7aac9"
+dependencies = [
+ "phf 0.11.3",
+ "phf_codegen 0.11.3",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+dependencies = [
+ "autocfg 0.1.8",
+ "libc",
+ "rand_chacha",
+ "rand_core 0.4.2",
+ "rand_hc",
+ "rand_isaac",
+ "rand_jitter",
+ "rand_os",
+ "rand_pcg",
+ "rand_xorshift",
+ "winapi",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rand_hc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_isaac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_jitter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
+dependencies = [
+ "libc",
+ "rand_core 0.4.2",
+ "winapi",
+]
+
+[[package]]
+name = "rand_os"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
+dependencies = [
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.4.2",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "roman"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2c543f0c827ae24df93159810fd4bce2d0abe3785bb4c4d68fae3c467d58d9b"
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
+[[package]]
+name = "siphasher"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
+
+[[package]]
+name = "sugars"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc0db74f9ee706e039d031a560bd7d110c7022f016051b3d33eeff9583e3e67a"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "textprocessing"
+version = "1.0.0"
+dependencies = [
+ "base64",
+ "bpmf_py",
+ "character_converter",
+ "chinese-number",
+ "hangeul_conversion",
+ "hanja",
+ "japanese",
+ "japanese_number_converter",
+ "num-bigint",
+ "num-traits",
+ "pinyin",
+ "pinyin_zhuyin",
+ "roman",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/wasm/tuningplayground/.gitignore b/wasm/tuningplayground/.gitignore
index ded49f5c..e86f3533 100644
--- a/wasm/tuningplayground/.gitignore
+++ b/wasm/tuningplayground/.gitignore
@@ -3,9 +3,8 @@ www-dev/
stable/
debug/
target/
-Cargo.lock
**/*.rs.bk
*.pdb
pkg/
venv
-__pycache__
\ No newline at end of file
+__pycache__
diff --git a/wasm/tuningplayground/Cargo.lock b/wasm/tuningplayground/Cargo.lock
new file mode 100644
index 00000000..ae96c881
--- /dev/null
+++ b/wasm/tuningplayground/Cargo.lock
@@ -0,0 +1,341 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chord_generator"
+version = "0.1.0"
+dependencies = [
+ "music21-rs",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "fraction"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872"
+dependencies = [
+ "lazy_static",
+ "num",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "keymapping"
+version = "1.0.0"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "music21-rs"
+version = "0.2.0"
+source = "git+https://github.com/float3/music21-rs.git#e626dfb823176dbd740b168f902098450a549893"
+dependencies = [
+ "fraction",
+ "itertools",
+ "num",
+ "num-traits",
+ "ordered-float",
+ "regex",
+ "serde",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "ordered-float"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tuning_systems"
+version = "1.0.0"
+
+[[package]]
+name = "tuningplayground"
+version = "1.0.0"
+dependencies = [
+ "keymapping",
+ "tuning_systems",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
+dependencies = [
+ "unicode-ident",
+]
diff --git a/wasm/tuningplayground/keymapping/.gitignore b/wasm/tuningplayground/keymapping/.gitignore
index d68534db..a326afe9 100644
--- a/wasm/tuningplayground/keymapping/.gitignore
+++ b/wasm/tuningplayground/keymapping/.gitignore
@@ -1,5 +1,4 @@
debug/
target/
-Cargo.lock
**/*.rs.bk
-*.pdb
\ No newline at end of file
+*.pdb
diff --git a/wasm/tuningplayground/src/lib.rs b/wasm/tuningplayground/src/lib.rs
index 3d4b8d4a..8990de18 100644
--- a/wasm/tuningplayground/src/lib.rs
+++ b/wasm/tuningplayground/src/lib.rs
@@ -151,6 +151,37 @@ fn split_chord_input(input: &str) -> Vec<&str> {
.collect()
}
+fn positive_integer_core(value: &str, fallback: usize) -> usize {
+ value
+ .parse::()
+ .ok()
+ .filter(|value| *value > 0)
+ .unwrap_or(fallback)
+}
+
+fn tuning_marked_hash_core(keys: &str) -> String {
+ let mut keys = keys
+ .split(',')
+ .filter_map(|key| key.trim().parse::().ok())
+ .collect::>();
+
+ keys.sort_unstable();
+ keys.dedup();
+ keys.into_iter()
+ .map(|key| key.to_string())
+ .collect::>()
+ .join(",")
+}
+
+fn tuning_hash_or_fallback_core(keys: &str, fallback_hash: &str) -> String {
+ let hash = tuning_marked_hash_core(keys);
+ if hash.is_empty() {
+ fallback_hash.trim_start_matches('#').to_string()
+ } else {
+ hash
+ }
+}
+
fn parse_octave(note: &str) -> Option {
let note = note.trim();
if let Some(index) = note.rfind('N') {
@@ -336,6 +367,19 @@ mod tests {
assert_eq!(chordname_from_bitmask(1).unwrap(), "C");
assert_eq!(chordname_from_bitmask(4095).unwrap(), "C-aggregate");
}
+
+ #[test]
+ fn parses_positive_integers_with_fallbacks() {
+ assert_eq!(positive_integer_core("24", 12), 24);
+ assert_eq!(positive_integer_core("0", 12), 12);
+ assert_eq!(positive_integer_core("nope", 12), 12);
+ }
+
+ #[test]
+ fn canonicalizes_marked_key_hashes() {
+ assert_eq!(tuning_marked_hash_core("5,3,5,-1"), "-1,3,5");
+ assert_eq!(tuning_hash_or_fallback_core("", "#12,14"), "12,14");
+ }
}
#[cfg(feature = "wasm")]
@@ -362,6 +406,24 @@ pub fn convert_notes(notes: Vec) -> String {
convert_notes_core(notes)
}
+#[cfg(feature = "wasm")]
+#[wasm_bindgen]
+pub fn tuning_positive_integer(value: &str, fallback: usize) -> usize {
+ positive_integer_core(value, fallback)
+}
+
+#[cfg(feature = "wasm")]
+#[wasm_bindgen]
+pub fn tuning_marked_hash(keys: &str) -> String {
+ tuning_marked_hash_core(keys)
+}
+
+#[cfg(feature = "wasm")]
+#[wasm_bindgen]
+pub fn tuning_hash_or_fallback(keys: &str, fallback_hash: &str) -> String {
+ tuning_hash_or_fallback_core(keys, fallback_hash)
+}
+
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn set_tuning_system(tuning_system: &str, octave_size: usize, step_size: usize) {
diff --git a/wasm/tuningplayground/tuning_systems/.gitignore b/wasm/tuningplayground/tuning_systems/.gitignore
index d68534db..a326afe9 100644
--- a/wasm/tuningplayground/tuning_systems/.gitignore
+++ b/wasm/tuningplayground/tuning_systems/.gitignore
@@ -1,5 +1,4 @@
debug/
target/
-Cargo.lock
**/*.rs.bk
-*.pdb
\ No newline at end of file
+*.pdb
diff --git a/wasm/wasm/.gitignore b/wasm/wasm/.gitignore
index 0e4338bb..8145a8a3 100644
--- a/wasm/wasm/.gitignore
+++ b/wasm/wasm/.gitignore
@@ -1,6 +1,5 @@
/target
**/*.rs.bk
-Cargo.lock
bin/
pkg/
www/
diff --git a/wasm/wasm/Cargo.lock b/wasm/wasm/Cargo.lock
new file mode 100644
index 00000000..4842a597
--- /dev/null
+++ b/wasm/wasm/Cargo.lock
@@ -0,0 +1,3241 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "adventofcode"
+version = "1.0.0"
+dependencies = [
+ "minify-html",
+ "syntect",
+ "walkdir",
+]
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "ansi-to-html"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12e283a4fc285735ef99577e81a125f738429516161ac33977e466d0d8d40764"
+dependencies = [
+ "regex",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "autocfg"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
+dependencies = [
+ "autocfg 1.5.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64-simd"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5"
+dependencies = [
+ "simd-abstraction",
+]
+
+[[package]]
+name = "base64-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
+dependencies = [
+ "outref 0.5.2",
+ "vsimd",
+]
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bpmf_py"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caae14d254ba113dec824a123282f4ef15077157c4c26517c80939cc415ee387"
+dependencies = [
+ "once_cell",
+ "sugars",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "character_converter"
+version = "2.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f399d4f1a0caebb329b79ea33c81afd6073e603de2589878a0770a9cef837797"
+dependencies = [
+ "bincode",
+ "fst",
+ "once_cell",
+]
+
+[[package]]
+name = "chinese-number"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e964125508474a83c95eb935697abbeb446ff4e9d62c71ce880e3986d1c606b"
+dependencies = [
+ "chinese-variant",
+ "enum-ordinalize",
+ "num-bigint",
+ "num-traits",
+]
+
+[[package]]
+name = "chinese-variant"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58b52a9840ffff5d4d0058ae529fa066a75e794e3125546acfc61c23ad755e49"
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "const-str"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3"
+dependencies = [
+ "const-str-proc-macro",
+]
+
+[[package]]
+name = "const-str-proc-macro"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "cow-utils"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf 0.11.3",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-color"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f"
+dependencies = [
+ "cssparser",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dashmap"
+version = "5.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
+dependencies = [
+ "cfg-if",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "data-url"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
+]
+
+[[package]]
+name = "dragonbox_ecma"
+version = "0.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d742b56656e8b14d63e7ea9806597b1849ae25412584c8adf78c0f67bd985e66"
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "fraction"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872"
+dependencies = [
+ "lazy_static",
+ "num",
+]
+
+[[package]]
+name = "fst"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a"
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "glsl"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "577ac945ce66a4b7004c6e2807f517f94ef027dda89df578abe1fbb979d22f49"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "glsl2hlsl"
+version = "0.1.0"
+dependencies = [
+ "glsl",
+ "md5",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "hangeul_conversion"
+version = "0.1.0"
+dependencies = [
+ "itertools 0.14.0",
+]
+
+[[package]]
+name = "hanja"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f231038f6d37b44fcb898f0348ff34d8b565fbf9ddec9e336c7019723679dde4"
+dependencies = [
+ "phf 0.7.24",
+ "phf_codegen 0.7.24",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash 0.7.8",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "japanese"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07694cc27451ccc3b98784ba4f16c6efc4503465b32f9336c2dff722a22d9db8"
+dependencies = [
+ "maplit",
+ "once_cell",
+]
+
+[[package]]
+name = "japanese_number_converter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e35ddfada703d296a21f6a843cbb64ccda3a0ac154b05f4f3eee6e0a31db175"
+
+[[package]]
+name = "js-sys"
+version = "0.3.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-escape-simd"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35e770254dd7802184595b1d30da2a15cb72569e2aca2b177aef8d22eac8a693"
+
+[[package]]
+name = "keymapping"
+version = "1.0.0"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "krabby"
+version = "0.3.1"
+source = "git+https://github.com/float3/krabby/#b65e48097921a9e28dda7d650bbefa6f2b30efd8"
+dependencies = [
+ "ansi-to-html",
+ "clap",
+ "dirs",
+ "rand 0.9.4",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "toml",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "lightningcss"
+version = "1.0.0-alpha.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb6314c2f0590ac93c86099b98bb7ba8abcf759bfd89604ffca906472bb54937"
+dependencies = [
+ "ahash 0.8.12",
+ "bitflags 2.11.1",
+ "const-str",
+ "cssparser",
+ "cssparser-color",
+ "dashmap",
+ "data-encoding",
+ "getrandom 0.3.4",
+ "indexmap",
+ "itertools 0.10.5",
+ "lazy_static",
+ "lightningcss-derive",
+ "parcel_selectors",
+ "parcel_sourcemap",
+ "pastey",
+ "pathdiff",
+ "rayon",
+ "serde",
+ "serde-content",
+ "smallvec",
+]
+
+[[package]]
+name = "lightningcss-derive"
+version = "1.0.0-alpha.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "md5"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mini-alloc"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b620761d4c29ede39bb1351c4360dba54d3b414d9fb460d37cba4c98b37a0b9d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "minicov"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
+dependencies = [
+ "cc",
+ "walkdir",
+]
+
+[[package]]
+name = "minify-html"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644c1511457e5f70dcbc03c932d4bd26351eceb8657b119c919dde2e65b6d120"
+dependencies = [
+ "ahash 0.8.12",
+ "aho-corasick",
+ "lightningcss",
+ "memchr",
+ "minify-html-common",
+ "once_cell",
+ "oxc_allocator",
+ "oxc_codegen",
+ "oxc_minifier",
+ "oxc_parser",
+ "oxc_span",
+]
+
+[[package]]
+name = "minify-html-common"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c3d71ae301c4e36949937ed9d121e1bf6847f2dc69ec20929b379d14659aac"
+dependencies = [
+ "ahash 0.8.12",
+ "aho-corasick",
+ "itertools 0.14.0",
+ "memchr",
+ "once_cell",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "music21-rs"
+version = "0.2.0"
+source = "git+https://github.com/float3/music21-rs.git#4dbc9ba98f1d9439f61779c65dca364d1563e51f"
+dependencies = [
+ "fraction",
+ "itertools 0.14.0",
+ "num",
+ "num-traits",
+ "ordered-float",
+ "regex",
+ "serde",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nonmax"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg 1.5.0",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg 1.5.0",
+ "libm",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "onig"
+version = "6.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
+dependencies = [
+ "bitflags 2.11.1",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-float"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "outref"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4"
+
+[[package]]
+name = "outref"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
+
+[[package]]
+name = "owo-colors"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
+
+[[package]]
+name = "oxc-browserslist"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb7a1163a5501f935f8722d839b576491b749c695e7a066aa0b8df988b806df"
+dependencies = [
+ "flate2",
+ "postcard",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "oxc-miette"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4356a61f2ed4c9b3610245215fbf48970eb277126919f87db9d0efa93a74245c"
+dependencies = [
+ "cfg-if",
+ "owo-colors",
+ "oxc-miette-derive",
+ "textwrap",
+ "thiserror 2.0.18",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "oxc-miette-derive"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b237422b014f8f8fff75bb9379e697d13f8d57551a22c88bebb39f073c1bf696"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "oxc_allocator"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "433214c659b860685d987ca25a523a544d35ebf87ee3658a942fd1c664cfa49b"
+dependencies = [
+ "allocator-api2",
+ "bumpalo",
+ "hashbrown 0.16.1",
+ "oxc_data_structures",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_ast"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e78ea25d23521092edcd509198635dd0bd96df7fb71349bcd8087bb8f6a615d"
+dependencies = [
+ "bitflags 2.11.1",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_estree",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_ast_macros"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3f29bf83925451a7ebbd30d3ff449414cc230c9b63aba70487c2ca74ea1e3ed"
+dependencies = [
+ "phf 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "oxc_ast_visit"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808862f42c9331954cf187261d6a48dbf471ccbe60b6a35f6b1056c11f32e95b"
+dependencies = [
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_codegen"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "480bab938439c921d34750708abf15aacf253ea11e2f348e4b085cbf697f4ea2"
+dependencies = [
+ "bitflags 2.11.1",
+ "cow-utils",
+ "dragonbox_ecma",
+ "itoa",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_index",
+ "oxc_semantic",
+ "oxc_sourcemap",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_compat"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd06b5cb59ece933be410adc6f811108a10c567fa2d105321f8d0f01cc4ab0c6"
+dependencies = [
+ "cow-utils",
+ "oxc-browserslist",
+ "oxc_syntax",
+ "rustc-hash",
+ "serde",
+]
+
+[[package]]
+name = "oxc_data_structures"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd090988aa69c1e394f424289abb9bb1281448a072419ca556a07228ad36b854"
+
+[[package]]
+name = "oxc_diagnostics"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b8a09558d38ee7f23faf0249cdb37b5b26f53a7375941f8636c41c661b283ce"
+dependencies = [
+ "cow-utils",
+ "oxc-miette",
+ "percent-encoding",
+]
+
+[[package]]
+name = "oxc_ecmascript"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0002ece1cc266aa6654913d39cf552b0cf501afce9d953b87c283dfcd307266"
+dependencies = [
+ "cow-utils",
+ "num-bigint",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_estree"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af85b24b7e10bf0ccb252f16d38492f51236c1ba146f62013993667cbf7fa649"
+
+[[package]]
+name = "oxc_index"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3e6120999627ec9703025eab7c9f410ebb7e95557632a8902ca48210416c2b"
+dependencies = [
+ "nonmax",
+ "serde",
+]
+
+[[package]]
+name = "oxc_mangler"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb751e1971f0809547347edfebe3755d5bed354aacf3ca7fe7216b901a11dd1d"
+dependencies = [
+ "itertools 0.14.0",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_index",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_minifier"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e836caa36befa6f9603eebf56b0c08d1e164ae31160c8e99ab3f535203065a4c"
+dependencies = [
+ "cow-utils",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_codegen",
+ "oxc_compat",
+ "oxc_data_structures",
+ "oxc_ecmascript",
+ "oxc_index",
+ "oxc_mangler",
+ "oxc_parser",
+ "oxc_regular_expression",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_syntax",
+ "oxc_traverse",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_parser"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb730ab93ff23bbc471ef7109f847afa709bb284dd52a5d3366283d724858158"
+dependencies = [
+ "bitflags 2.11.1",
+ "cow-utils",
+ "memchr",
+ "num-bigint",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+ "seq-macro",
+]
+
+[[package]]
+name = "oxc_regular_expression"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27bafc3035e3b0fe555ae56f7bbc108a7ee520b0858250ba16d197e44ca83746"
+dependencies = [
+ "bitflags 2.11.1",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_diagnostics",
+ "oxc_span",
+ "phf 0.13.1",
+ "rustc-hash",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "oxc_semantic"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578e5fa0b975a28e1d1ffa6c81c8df094545fe3c3225956d4e93ffe1ade3dae0"
+dependencies = [
+ "itertools 0.14.0",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_index",
+ "oxc_span",
+ "oxc_syntax",
+ "phf 0.13.1",
+ "rustc-hash",
+ "self_cell",
+]
+
+[[package]]
+name = "oxc_sourcemap"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d378eb8bad20e89d66276aebab51f6a5408571092cac94abdd3eabb773713d6"
+dependencies = [
+ "base64-simd 0.8.0",
+ "json-escape-simd",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "oxc_span"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71243fe1ece27f71a6be5e2ab11d051792eeb578f3b056d7c9bbe82ec8043bf3"
+dependencies = [
+ "compact_str",
+ "oxc-miette",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_estree",
+]
+
+[[package]]
+name = "oxc_syntax"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93c99e555ed497111004a71fb2aa6f8fb90b0d3e2733aceeb035e24701d69634"
+dependencies = [
+ "bitflags 2.11.1",
+ "cow-utils",
+ "dragonbox_ecma",
+ "nonmax",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_data_structures",
+ "oxc_estree",
+ "oxc_index",
+ "oxc_span",
+ "phf 0.13.1",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "oxc_traverse"
+version = "0.95.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68d00264248ef474988ea02bfa44ccacd9a1d8716f0efb33a9cd6f992cf07b7b"
+dependencies = [
+ "itoa",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_data_structures",
+ "oxc_ecmascript",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+]
+
+[[package]]
+name = "parcel_selectors"
+version = "0.28.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196"
+dependencies = [
+ "bitflags 2.11.1",
+ "cssparser",
+ "log",
+ "phf 0.11.3",
+ "phf_codegen 0.11.3",
+ "precomputed-hash",
+ "rustc-hash",
+ "smallvec",
+]
+
+[[package]]
+name = "parcel_sourcemap"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb"
+dependencies = [
+ "base64-simd 0.7.0",
+ "data-url",
+ "rkyv",
+ "serde",
+ "serde_json",
+ "vlq",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
+dependencies = [
+ "phf_shared 0.7.24",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros 0.13.1",
+ "phf_shared 0.13.1",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e"
+dependencies = [
+ "phf_generator 0.7.24",
+ "phf_shared 0.7.24",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662"
+dependencies = [
+ "phf_shared 0.7.24",
+ "rand 0.6.5",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared 0.11.3",
+ "rand 0.8.6",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared 0.13.1",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.7.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
+dependencies = [
+ "siphasher 0.2.3",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher 1.0.2",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher 1.0.2",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pinyin"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e225f595052d9c46045755be4b8d7950b6d9f3c33e0c0b74ba58f11bbfa8c64b"
+
+[[package]]
+name = "pinyin_zhuyin"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "002d90054842a688ae09569adbf07c9e4868ec19a8b44700299867a28cd7aac9"
+dependencies = [
+ "phf 0.11.3",
+ "phf_codegen 0.11.3",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plist"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
+dependencies = [
+ "base64",
+ "indexmap",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.39.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+dependencies = [
+ "autocfg 0.1.8",
+ "libc",
+ "rand_chacha 0.1.1",
+ "rand_core 0.4.2",
+ "rand_hc",
+ "rand_isaac",
+ "rand_jitter",
+ "rand_os",
+ "rand_pcg",
+ "rand_xorshift",
+ "winapi",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_isaac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_jitter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
+dependencies = [
+ "libc",
+ "rand_core 0.4.2",
+ "winapi",
+]
+
+[[package]]
+name = "rand_os"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
+dependencies = [
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.4.2",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "roman"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2c543f0c827ae24df93159810fd4bce2d0abe3785bb4c4d68fae3c467d58d9b"
+
+[[package]]
+name = "rust-embed"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.117",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "self_cell"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-content"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "simd-abstraction"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987"
+dependencies = [
+ "outref 0.1.0",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
+[[package]]
+name = "siphasher"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "smawk"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "sugars"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc0db74f9ee706e039d031a560bd7d110c7022f016051b3d33eeff9583e3e67a"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syntect"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
+dependencies = [
+ "bincode",
+ "flate2",
+ "fnv",
+ "once_cell",
+ "onig",
+ "plist",
+ "regex-syntax",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "thiserror 2.0.18",
+ "walkdir",
+ "yaml-rust",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "textprocessing"
+version = "1.0.0"
+dependencies = [
+ "base64",
+ "bpmf_py",
+ "character_converter",
+ "chinese-number",
+ "hangeul_conversion",
+ "hanja",
+ "japanese",
+ "japanese_number_converter",
+ "num-bigint",
+ "num-traits",
+ "pinyin",
+ "pinyin_zhuyin",
+ "roman",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tuning_systems"
+version = "1.0.0"
+
+[[package]]
+name = "tuningplayground"
+version = "1.0.0"
+dependencies = [
+ "keymapping",
+ "tuning_systems",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
+
+[[package]]
+name = "unicode-id-start"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vlq"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
+
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm"
+version = "0.1.0"
+dependencies = [
+ "adventofcode",
+ "console_error_panic_hook",
+ "getrandom 0.3.4",
+ "getrandom 0.4.2",
+ "glsl2hlsl",
+ "krabby",
+ "mini-alloc",
+ "music21-rs",
+ "textprocessing",
+ "tuningplayground",
+ "unicode-width",
+ "wasm-bindgen",
+ "wasm-bindgen-test",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-bindgen-test"
+version = "0.3.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29826f9d9ecaa314c480d376b276d1c790e6cb6a4681fab8532da69cbabf977d"
+dependencies = [
+ "async-trait",
+ "cast",
+ "js-sys",
+ "libm",
+ "minicov",
+ "nu-ansi-term",
+ "num-traits",
+ "oorandom",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-bindgen-test-macro",
+ "wasm-bindgen-test-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-test-macro"
+version = "0.3.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c610311887f9e6599a546d278d12d69dfd3a3e92639b2129e4b11ad6cf1961d6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "wasm-bindgen-test-shared"
+version = "0.2.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60238e5b4b1b295701d6f9a66d2a126fe19990348f5fb9dae3b623a370119d94"
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.11.1",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.1",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/wasm/wasm/src/bayes.rs b/wasm/wasm/src/bayes.rs
index b439ec5c..55d0ec5e 100644
--- a/wasm/wasm/src/bayes.rs
+++ b/wasm/wasm/src/bayes.rs
@@ -83,6 +83,11 @@ pub fn solve_bayes_percent(
}
}
+#[wasm_bindgen]
+pub fn bayes_number(value: f64) -> String {
+ format_number(value)
+}
+
fn percent_to_probability(value: f64) -> f64 {
clamp_probability(value / 100.0)
}
@@ -95,6 +100,26 @@ fn clamp_probability(value: f64) -> f64 {
value.clamp(0.0, 1.0)
}
+fn format_number(value: f64) -> String {
+ if !value.is_finite() {
+ return "undefined".to_string();
+ }
+
+ let mut rounded = format!("{value:.6}");
+ while rounded.contains('.') && rounded.ends_with('0') {
+ rounded.pop();
+ }
+ if rounded.ends_with('.') {
+ rounded.pop();
+ }
+
+ if rounded == "-0" {
+ "0".to_string()
+ } else {
+ rounded
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -113,4 +138,11 @@ mod tests {
let result = solve_bayes_percent(1.0, 90.0, 5.0, 0.0, false);
assert_eq!(result.error_code, 1);
}
+
+ #[test]
+ fn formats_numbers_like_the_browser_solver() {
+ assert_eq!(format_number(1.230000), "1.23");
+ assert_eq!(format_number(-0.0000001), "0");
+ assert_eq!(format_number(f64::NAN), "undefined");
+ }
}
diff --git a/wasm/wasm/src/chars.rs b/wasm/wasm/src/chars.rs
index 4241a4ee..4de7fbed 100644
--- a/wasm/wasm/src/chars.rs
+++ b/wasm/wasm/src/chars.rs
@@ -129,11 +129,7 @@ fn random_u32(max: u32) -> u32 {
fn random_kanji(remove: bool) -> char {
let mut v = KANJI.lock().expect("failed to lock kanji");
let idx = random_u32(v.len() as u32) as usize;
- if remove {
- v.remove(idx)
- } else {
- v[idx]
- }
+ if remove { v.remove(idx) } else { v[idx] }
}
// fn random_bopomofo(remove: bool) -> char {
diff --git a/wasm/wasm/src/glsl.rs b/wasm/wasm/src/glsl.rs
index b43052da..fe82de47 100644
--- a/wasm/wasm/src/glsl.rs
+++ b/wasm/wasm/src/glsl.rs
@@ -19,6 +19,15 @@ pub fn transpile(input: String, extract_props: bool, raymarch: bool) -> String {
)
}
+#[wasm_bindgen]
+pub fn shader_id_from_url(input: &str) -> String {
+ input
+ .rsplit('/')
+ .find(|part| !part.is_empty())
+ .unwrap_or_default()
+ .to_string()
+}
+
#[wasm_bindgen]
pub fn download(json: String, extract_props: bool, raymarch: bool) {
use glsl2hlsl::get_files;
@@ -36,3 +45,22 @@ pub fn download(json: String, extract_props: bool, raymarch: bool) {
downloadImage(&f.name, &f.contents);
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn extracts_shader_id_from_url_or_raw_id() {
+ assert_eq!(
+ shader_id_from_url("https://www.shadertoy.com/view/Xd3GRf"),
+ "Xd3GRf"
+ );
+ assert_eq!(
+ shader_id_from_url("https://www.shadertoy.com/view/Xd3GRf/"),
+ "Xd3GRf"
+ );
+ assert_eq!(shader_id_from_url("Xd3GRf"), "Xd3GRf");
+ assert_eq!(shader_id_from_url(""), "");
+ }
+}
diff --git a/wasm/wasm/src/lib.rs b/wasm/wasm/src/lib.rs
index 4887eab1..bd101ac6 100644
--- a/wasm/wasm/src/lib.rs
+++ b/wasm/wasm/src/lib.rs
@@ -7,7 +7,11 @@ pub mod chars;
pub mod glsl;
pub mod graph;
pub mod krabby;
+pub mod movies;
+pub mod photography;
pub mod polyrhythm;
+pub mod random;
+pub mod trolley;
use wasm_bindgen::prelude::*;
diff --git a/wasm/wasm/src/movies.rs b/wasm/wasm/src/movies.rs
new file mode 100644
index 00000000..16eb98e7
--- /dev/null
+++ b/wasm/wasm/src/movies.rs
@@ -0,0 +1,39 @@
+use wasm_bindgen::prelude::wasm_bindgen;
+
+#[wasm_bindgen]
+pub fn movie_candidate_is_visible(line: &str) -> bool {
+ !line.contains("BREAK") && !has_drop_date(line)
+}
+
+fn has_drop_date(line: &str) -> bool {
+ let suffix: Vec = line.chars().rev().take(12).collect();
+ if suffix.len() != 12 {
+ return false;
+ }
+
+ suffix[0] == ')'
+ && suffix[1].is_ascii_digit()
+ && suffix[2].is_ascii_digit()
+ && suffix[3] == '-'
+ && suffix[4].is_ascii_digit()
+ && suffix[5].is_ascii_digit()
+ && suffix[6] == '-'
+ && suffix[7].is_ascii_digit()
+ && suffix[8].is_ascii_digit()
+ && suffix[9].is_ascii_digit()
+ && suffix[10].is_ascii_digit()
+ && suffix[11] == '('
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn filters_break_markers_and_dated_drops() {
+ assert!(movie_candidate_is_visible("Tampopo"));
+ assert!(movie_candidate_is_visible("Movie (1985)"));
+ assert!(!movie_candidate_is_visible("BREAK"));
+ assert!(!movie_candidate_is_visible("Tampopo (2026-05-04)"));
+ }
+}
diff --git a/wasm/wasm/src/photography.rs b/wasm/wasm/src/photography.rs
new file mode 100644
index 00000000..d69c241f
--- /dev/null
+++ b/wasm/wasm/src/photography.rs
@@ -0,0 +1,49 @@
+use wasm_bindgen::prelude::wasm_bindgen;
+
+#[wasm_bindgen]
+pub fn photo_count_label(count: usize) -> String {
+ let suffix = if count == 1 { "" } else { "s" };
+ format!("{count} photo{suffix}")
+}
+
+#[wasm_bindgen]
+pub fn photo_caption(title: &str, meta: &str) -> String {
+ let title = title.trim();
+ let meta = meta.trim();
+
+ match (title.is_empty(), meta.is_empty()) {
+ (true, true) => String::new(),
+ (false, true) => title.to_string(),
+ (true, false) => meta.to_string(),
+ (false, false) => format!("{title} - {meta}"),
+ }
+}
+
+#[wasm_bindgen]
+pub fn photo_manifest_entry_is_valid(src: &str, title: &str) -> bool {
+ !src.trim().is_empty() && !title.trim().is_empty()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn pluralizes_photo_count() {
+ assert_eq!(photo_count_label(1), "1 photo");
+ assert_eq!(photo_count_label(2), "2 photos");
+ }
+
+ #[test]
+ fn joins_caption_parts() {
+ assert_eq!(photo_caption("frame", ""), "frame");
+ assert_eq!(photo_caption("frame", "35mm"), "frame - 35mm");
+ }
+
+ #[test]
+ fn validates_manifest_entry_shape() {
+ assert!(photo_manifest_entry_is_valid("/photo.jpg", "photo"));
+ assert!(!photo_manifest_entry_is_valid("", "photo"));
+ assert!(!photo_manifest_entry_is_valid("/photo.jpg", ""));
+ }
+}
diff --git a/wasm/wasm/src/polyrhythm.rs b/wasm/wasm/src/polyrhythm.rs
index 41204225..ccbbc501 100644
--- a/wasm/wasm/src/polyrhythm.rs
+++ b/wasm/wasm/src/polyrhythm.rs
@@ -50,6 +50,21 @@ fn compute_position_along_polygon(vertices: &[(f64, f64)], t: f64) -> (f64, f64)
)
}
+fn voice_color(index: usize) -> &'static str {
+ const COLORS: [&str; 8] = [
+ "#4f8cff", "#ffbc3f", "#fe7fb3", "#61d394", "#dd6fff", "#4dd0e1", "#f87171", "#a3e635",
+ ];
+ COLORS[index % COLORS.len()]
+}
+
+fn voice_frequency(base_pitch: u32, index: usize) -> f32 {
+ const INTERVALS: [i32; 8] = [0, 7, 12, 16, 19, 24, 28, 31];
+ let octave_offset = (index / INTERVALS.len()) as i32 * 24;
+ let semitones = INTERVALS[index % INTERVALS.len()] + octave_offset;
+ let frequency = base_pitch as f32 * 2.0_f32.powf(semitones as f32 / 12.0);
+ frequency.min(4_000.0)
+}
+
type MyType = Rc>>>;
/// Starts the animation with user settings.
@@ -125,7 +140,7 @@ pub fn start_with_settings(
let context2_rc = Rc::new(context2);
let canvas2_rc = Rc::new(canvas2);
- // For each voice, track the last triggered beat (None initially).
+ // For each voice, track the last triggered (measure, beat) pair.
let beat_tracker = Rc::new(RefCell::new(vec![None; poly_rc.components.len()]));
// Create an Rc for the animation closure.
@@ -139,6 +154,7 @@ pub fn start_with_settings(
let current_time = js_sys::Date::now();
let elapsed = (current_time - start_time) / 1000.0; // seconds
let t_phase = (elapsed % measure_duration) / measure_duration;
+ let measure_index = (elapsed / measure_duration).floor() as u64;
// Clear canvases.
let cvs0 = &*canvas0_rc;
@@ -155,19 +171,14 @@ pub fn start_with_settings(
let center_x = cvs1.width() as f64 / 2.0;
let center_y = cvs1.height() as f64 / 2.0;
- // Reset beat tracking at the start of each measure.
- if t_phase < 0.05 {
- let mut tracker = beat_tracker.borrow_mut();
- for trig in tracker.iter_mut() {
- *trig = None;
- }
- }
-
// For each voice in the polyrhythm.
for (i, &sides) in poly_rc.components.iter().enumerate() {
+ let color = voice_color(i);
let radius = 40.0 + (i * 2) as f64 * 30.0;
let vertices = compute_polygon_vertices(center_x, center_y, radius, sides as usize);
ctx1.begin_path();
+ ctx1.set_stroke_style_str(color);
+ ctx1.set_line_width(2.0);
if let Some(&(x, y)) = vertices.first() {
ctx1.move_to(x, y);
for &(vx, vy) in vertices.iter().skip(1) {
@@ -178,59 +189,72 @@ pub fn start_with_settings(
}
ctx1.stroke();
}
+ ctx1.set_fill_style_str(color);
+ for &(vx, vy) in &vertices {
+ ctx1.begin_path();
+ ctx1.arc(vx, vy, 3.0, 0.0, std::f64::consts::PI * 2.0)
+ .expect("Failed to draw polygon vertex");
+ ctx1.fill();
+ }
let pos = compute_position_along_polygon(&vertices, t_phase);
ctx1.begin_path();
- ctx1.arc(pos.0, pos.1, 5.0, 0.0, std::f64::consts::PI * 2.0)
+ ctx1.arc(pos.0, pos.1, 6.0, 0.0, std::f64::consts::PI * 2.0)
.expect("Failed to draw moving point");
ctx1.fill();
// --- Beat Counting Visualization ---
- let beat_disp = ((t_phase * sides as f64).floor() as u32) + 1;
- ctx0.set_font("20px sans-serif");
- ctx0.set_fill_style_str("blue");
+ let beat_index = ((t_phase * sides as f64).floor() as u32).min(sides.saturating_sub(1));
+ let beat_disp = beat_index + 1;
+ ctx0.set_font("18px sans-serif");
+ ctx0.set_fill_style_str(color);
ctx0.fill_text(
- &beat_disp.to_string(),
- center_x - 10.0,
- center_y - (70.0 + i as f64 * 30.0) - 10.0,
+ &format!("{} hits: {}", sides, beat_disp),
+ 24.0,
+ 30.0 + i as f64 * 26.0,
)
.expect("Failed to draw beat number");
+ // --- Timeline visualization ---
+ let timeline_left = 26.0;
+ let timeline_width = cvs2.width() as f64 - 110.0;
+ let timeline_y = 34.0 + i as f64 * 28.0;
+ ctx2.set_stroke_style_str("#667085");
+ ctx2.set_line_width(1.0);
+ ctx2.begin_path();
+ ctx2.move_to(timeline_left, timeline_y);
+ ctx2.line_to(timeline_left + timeline_width, timeline_y);
+ ctx2.stroke();
+
+ for beat in 0..sides {
+ let x = timeline_left + timeline_width * beat as f64 / sides as f64;
+ ctx2.begin_path();
+ ctx2.set_fill_style_str(if beat == beat_index { color } else { "#667085" });
+ ctx2.arc(
+ x,
+ timeline_y,
+ if beat == beat_index { 5.0 } else { 3.0 },
+ 0.0,
+ std::f64::consts::PI * 2.0,
+ )
+ .expect("Failed to draw timeline beat");
+ ctx2.fill();
+ }
+ ctx2.set_font("14px sans-serif");
+ ctx2.set_fill_style_str(color);
+ ctx2.fill_text(
+ &format!("voice {}", i + 1),
+ timeline_left + timeline_width + 14.0,
+ timeline_y + 5.0,
+ )
+ .expect("Failed to draw timeline label");
+
// --- Trigger beep per beat for this voice ---
- let vertex_interval = measure_duration / (sides as f64);
- let measure_elapsed = elapsed % measure_duration;
- let measure_start_time = current_time - (measure_elapsed * 1000.0);
- // Compute current beat index (0-indexed).
- let beat_index = (t_phase * sides as f64).floor() as u32;
- // Compute the scheduled start time for this beat.
- let beat_start_time =
- measure_start_time + (beat_index as f64 * vertex_interval * 1000.0);
- let delay = (beat_start_time - current_time) as i32;
- let freq = pitch as f32 + (i as f32 * 20.0);
-
- // Check if this beat hasn't been triggered yet.
- if beat_tracker.borrow()[i] != Some(beat_index) {
- let tracker_clone = beat_tracker.clone();
- let window_inner = window_clone.clone();
- let i_clone = i;
- // Closure to trigger the beep and update tracker.
- let beep_closure = Closure::wrap(Box::new(move || {
- play_beep(freq, 0.1);
- tracker_clone.borrow_mut()[i_clone] = Some(beat_index);
- }) as Box);
-
- if delay > 0 {
- window_inner
- .set_timeout_with_callback_and_timeout_and_arguments_0(
- beep_closure.as_ref().unchecked_ref(),
- delay,
- )
- .expect("Failed to schedule beep");
- } else {
- // If we're already past the scheduled time, trigger immediately.
- play_beep(freq, 0.1);
- beat_tracker.borrow_mut()[i] = Some(beat_index);
- }
- beep_closure.forget();
+ let freq = voice_frequency(pitch, i);
+ let beat_key = (measure_index, beat_index);
+
+ if beat_tracker.borrow()[i] != Some(beat_key) {
+ play_beep(freq, if beat_index == 0 { 0.13 } else { 0.09 });
+ beat_tracker.borrow_mut()[i] = Some(beat_key);
}
}
@@ -266,3 +290,20 @@ pub fn stop() {
}
});
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn voice_frequencies_are_related_to_the_base_pitch() {
+ assert!((voice_frequency(440, 0) - 440.0).abs() < 0.001);
+ assert!((voice_frequency(440, 1) - 659.255).abs() < 0.01);
+ assert!(voice_frequency(440, 3) > voice_frequency(440, 2));
+ }
+
+ #[test]
+ fn voice_colors_cycle() {
+ assert_eq!(voice_color(0), voice_color(8));
+ }
+}
diff --git a/wasm/wasm/src/random.rs b/wasm/wasm/src/random.rs
new file mode 100644
index 00000000..171ecae9
--- /dev/null
+++ b/wasm/wasm/src/random.rs
@@ -0,0 +1,41 @@
+use wasm_bindgen::prelude::wasm_bindgen;
+
+#[wasm_bindgen]
+pub fn random_index(count: u32) -> u32 {
+ random_index_core(count)
+}
+
+#[wasm_bindgen]
+pub fn random_range(min: f64, max: f64) -> f64 {
+ if !min.is_finite() || !max.is_finite() || max <= min {
+ return min;
+ }
+
+ let unit = getrandom::u32().map_or(0.0, |value| value as f64 / (u32::MAX as f64 + 1.0));
+ min + unit * (max - min)
+}
+
+pub(crate) fn random_index_core(count: u32) -> u32 {
+ if count == 0 {
+ return 0;
+ }
+
+ getrandom::u32().map_or(0, |value| value % count)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn keeps_empty_random_index_inert() {
+ assert_eq!(random_index(0), 0);
+ }
+
+ #[test]
+ fn handles_invalid_random_ranges_without_entropy() {
+ assert_eq!(random_range(4.0, 4.0), 4.0);
+ assert_eq!(random_range(5.0, 4.0), 5.0);
+ assert!(random_range(f64::NAN, 4.0).is_nan());
+ }
+}
diff --git a/wasm/wasm/src/trolley.rs b/wasm/wasm/src/trolley.rs
new file mode 100644
index 00000000..506f11c4
--- /dev/null
+++ b/wasm/wasm/src/trolley.rs
@@ -0,0 +1,40 @@
+use crate::random::random_index_core;
+use wasm_bindgen::prelude::wasm_bindgen;
+
+#[wasm_bindgen]
+pub fn trolley_random_index(count: u32) -> u32 {
+ if count == 0 {
+ return 0;
+ }
+
+ random_index_core(count) + 1
+}
+
+#[wasm_bindgen]
+pub fn trolley_media_src(base_path: &str, index: u32, extension: &str) -> String {
+ let separator = if base_path.ends_with('/') { "" } else { "/" };
+ let extension = extension.trim_start_matches('.');
+ format!("{base_path}{separator}{index:02}.{extension}")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn keeps_zero_count_random_index_inert() {
+ assert_eq!(trolley_random_index(0), 0);
+ }
+
+ #[test]
+ fn builds_padded_trolley_media_paths() {
+ assert_eq!(
+ trolley_media_src("/misc/trolley", 7, "jpg"),
+ "/misc/trolley/07.jpg"
+ );
+ assert_eq!(
+ trolley_media_src("/misc/trolley/", 12, ".mp4"),
+ "/misc/trolley/12.mp4"
+ );
+ }
+}