fasrt 0.1.0

A blazing fast, zero-copy subtitle parser and writer for SRT and WebVTT in Rust.
use std::{
  env::{self, var},
  fs::OpenOptions,
  io::Write,
  path::PathBuf,
};

fn main() {
  // Don't rerun this on changes other than build.rs, as we only depend on
  // the rustc version.
  println!("cargo:rerun-if-changed=build.rs");

  // Check for `--features=tarpaulin`.
  let tarpaulin = var("CARGO_FEATURE_TARPAULIN").is_ok();

  if tarpaulin {
    use_feature("tarpaulin");
  } else {
    // Always rerun if these env vars change.
    println!("cargo:rerun-if-env-changed=CARGO_TARPAULIN");
    println!("cargo:rerun-if-env-changed=CARGO_CFG_TARPAULIN");

    // Detect tarpaulin by environment variable
    if env::var("CARGO_TARPAULIN").is_ok() || env::var("CARGO_CFG_TARPAULIN").is_ok() {
      use_feature("tarpaulin");
    }
  }

  generate_from_str_60("minute");
  generate_to_str_60("minute");

  generate_from_str_60("second");
  generate_to_str_60("second");

  generate_hour_from_str();
  generate_hour_to_str();

  generate_millis_from_str();
  generate_millis_to_str();

  generate_html5_entities();

  // Rerun this script if any of our features or configuration flags change,
  // or if the toolchain we used for feature detection changes.
  println!("cargo:rerun-if-env-changed=CARGO_FEATURE_TARPAULIN");
}

fn use_feature(feature: &str) {
  println!("cargo:rustc-cfg={}", feature);
}

fn generate_from_str_60(name: &str) {
  let mut out = String::new();

  for i in 0..10 {
    out.push_str(&format!(
      "      \"{}\" => return ::core::result::Result::Err(Self::Err::NotPadded),\n",
      i
    ));
  }

  for i in 0..60 {
    out.push_str(&format!("      \"{:02}\" => {},\n", i, i));
  }

  let output = format!(
    r###"
// @generated by build.rs - do not edit by hand

macro_rules! {name}_from_str {{
  ($value:expr) => {{{{
    ::core::result::Result::Ok(Self(match $value {{
{}     _ => {{
        let val = <::core::primitive::u8 as ::core::str::FromStr>::from_str($value)?;
        return ::core::result::Result::Err(Self::Err::Overflow(val));
      }},
    }}))
  }}}}
}}

pub(crate) use {name}_from_str;
"###,
    out
  );

  let path = PathBuf::from("generated").join(format!("{}_from_str.rs", name));
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .open(path)
    .expect("failed to open generated file");
  file
    .write_all(output.as_bytes())
    .expect("failed to write generated file");
}

fn generate_to_str_60(name: &str) {
  let mut out = String::new();

  for i in 0..60 {
    out.push_str(&format!("      {} => \"{:02}\",\n", i, i));
  }

  let output = format!(
    r###"
// @generated by build.rs - do not edit by hand

macro_rules! {name}_to_str {{
  ($value:expr) => {{{{
    match $value {{
{}     _ => ::core::panic!("{} value must be between 00-59"),
    }}
  }}}}
}}

pub(crate) use {name}_to_str;
"###,
    out, name
  );

  let path = PathBuf::from("generated").join(format!("{}_to_str.rs", name));
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .open(path)
    .expect("failed to open generated file");
  file
    .write_all(output.as_bytes())
    .expect("failed to write generated file");
}

fn generate_millis_from_str() {
  let mut out = String::new();

  for i in 0..10 {
    out.push_str(&format!(
      "      \"{}\" => return ::core::result::Result::Err(Self::Err::NotPadded),\n",
      i
    ));
  }

  for i in 0..100 {
    out.push_str(&format!(
      "      \"{:02}\" => return ::core::result::Result::Err(Self::Err::NotPadded),\n",
      i
    ));
  }

  for i in 0..1000 {
    out.push_str(&format!("      \"{:03}\" => {},\n", i, i));
  }

  let output = format!(
    r###"
// @generated by build.rs - do not edit by hand

macro_rules! millisecond_from_str {{
  ($value:expr) => {{{{
    ::core::result::Result::Ok(Self(match $value {{
{}     _ => {{
        let val = <::core::primitive::u16 as ::core::str::FromStr>::from_str($value)?;
        return ::core::result::Result::Err(Self::Err::Overflow(val));
      }},
    }}))
  }}}}
}}

pub(crate) use millisecond_from_str;
"###,
    out
  );

  let path = PathBuf::from("generated").join("millisecond_from_str.rs");
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .open(path)
    .expect("failed to open generated file");
  file
    .write_all(output.as_bytes())
    .expect("failed to write generated file");
}

fn generate_millis_to_str() {
  let mut out = String::new();

  for i in 0..1000 {
    out.push_str(&format!("      {} => \"{:03}\",\n", i, i));
  }

  let output = format!(
    r###"
// @generated by build.rs - do not edit by hand

macro_rules! millisecond_to_str {{
  ($value:expr) => {{{{
    match $value {{
{}     _ => ::core::panic!("Millisecond value must be between 000-999"),
    }}
  }}}}
}}

pub(crate) use millisecond_to_str;
"###,
    out,
  );

  let path = PathBuf::from("generated").join("millisecond_to_str.rs");
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .open(path)
    .expect("failed to open generated file");
  file
    .write_all(output.as_bytes())
    .expect("failed to write generated file");
}

fn generate_hour_from_str() {
  let mut out = String::new();

  for i in 0..10 {
    out.push_str(&format!(
      "      \"{}\" => return ::core::result::Result::Err(Self::Err::NotPadded),\n",
      i
    ));
  }

  for i in 0..100 {
    out.push_str(&format!("      \"{i:02}\" => {i},\n"));
  }

  for i in 100..1000 {
    out.push_str(&format!("      \"{i}\" => {i},\n"));
  }

  let output = format!(
    r###"
// @generated by build.rs - do not edit by hand

macro_rules! hour_from_str {{
  ($value:expr) => {{{{
    ::core::result::Result::Ok(Self(match $value {{
{}     _ => {{
        let val = <::core::primitive::u16 as ::core::str::FromStr>::from_str($value)?;
        return ::core::result::Result::Err(Self::Err::Overflow(val));
      }},
    }}))
  }}}}
}}

pub(crate) use hour_from_str;
"###,
    out
  );

  let path = PathBuf::from("generated").join("hour_from_str.rs");
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .open(path)
    .expect("failed to open generated file");
  file
    .write_all(output.as_bytes())
    .expect("failed to write generated file");
}

fn generate_hour_to_str() {
  let mut out = String::new();

  for i in 0..1000 {
    out.push_str(&format!("      {} => \"{:02}\",\n", i, i));
  }

  let output = format!(
    r###"
// @generated by build.rs - do not edit by hand

macro_rules! hour_to_str {{
  ($value:expr) => {{{{
    match $value {{
{}     _ => ::core::panic!("Hour value must be between 00-999"),
    }}
  }}}}
}}

pub(crate) use hour_to_str;
"###,
    out,
  );

  let path = PathBuf::from("generated").join("hour_to_str.rs");
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .open(path)
    .expect("failed to open generated file");
  file
    .write_all(output.as_bytes())
    .expect("failed to write generated file");
}

fn generate_html5_entities() {
  println!("cargo:rerun-if-changed=assets/html5_entities.json");

  let json = std::fs::read_to_string("assets/html5_entities.json")
    .expect("failed to read html5_entities.json");

  let entries: Vec<(String, Vec<u32>)> =
    serde_json::from_str(&json).expect("failed to parse html5_entities.json");

  let dest = PathBuf::from("generated").join("html5_entities.rs");
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .open(dest)
    .expect("failed to create html5_entities.rs");

  writeln!(
    file,
    "// @generated by build.rs from assets/html5_entities.json — do not edit"
  )
  .unwrap();
  writeln!(file).unwrap();

  let values: Vec<(String, String)> = entries
    .iter()
    .map(|(name, cps)| {
      let escaped: String = cps
        .iter()
        .filter_map(|&cp| char::from_u32(cp))
        .map(|c| format!("\\u{{{:04X}}}", c as u32))
        .collect();
      (name.clone(), format!("\"{}\"", escaped))
    })
    .collect();

  let mut builder = phf_codegen::Map::new();
  for (name, value) in &values {
    builder.entry(name.as_str(), value.as_str());
  }

  writeln!(
    file,
    "pub(crate) static HTML5_ENTITIES: phf::Map<&'static str, &'static str> = {};",
    builder.build()
  )
  .unwrap();
}