,
url_name: &str,
) -> Result<&mut Self> {
let path = &self.path_for(path);
let ext = name_and_ext(path).map_or("", |(_, e)| e);
println!("cargo:rerun-if-changed={}", path.display());
self.add_static(path, url_name, url_name, &FileContent(path), ext)?;
Ok(self)
}
/// Add a resource by its name and content, without reading an actual file.
///
/// The `path` parameter is used only to create a file name, the actual
/// content of the static file will be the `data` parameter.
/// A hash will be added to the file name, just as for
/// file-sourced statics.
///
/// # Examples
///
/// With the folloing code in `build.rs`:
/// ````
/// # use ructe::{Result, Ructe, StaticFiles};
/// # use std::fs::create_dir_all;
/// # use std::path::PathBuf;
/// # use std::vec::Vec;
/// # fn main() -> Result<()> {
/// # let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target").join("test-tmp").join("add-file");
/// # create_dir_all(&p);
/// # let mut ructe = Ructe::new(p)?;
/// let mut statics = ructe.statics()?;
/// statics.add_file_data("black.css", b"body{color:black}\n");
/// # Ok(())
/// # }
/// ````
///
/// A `StaticFile` named `black_css` will be defined in the
/// `templates::statics` module of your crate:
///
/// ````
/// # mod statics {
/// # use ructe::templates::StaticFile;
/// # pub static black_css: StaticFile = StaticFile {
/// # content: b"body{color:black}\n",
/// # name: "black-r3rltVhW.css",
/// # #[cfg(feature = "mime03")]
/// # mime: &mime::TEXT_CSS,
/// # };
/// # }
/// assert_eq!(statics::black_css.name, "black-r3rltVhW.css");
/// ````
pub fn add_file_data(
&mut self,
path: P,
data: &[u8],
) -> Result<&mut Self>
where
P: AsRef,
{
let path = &self.path_for(path);
if let Some((name, ext)) = name_and_ext(path) {
let rust_name = format!("{name}_{ext}");
let url_name = format!("{name}-{}.{ext}", checksum_slug(data));
self.add_static(
path,
&rust_name,
&url_name,
&ByteString(data),
ext,
)?;
}
Ok(self)
}
/// Compile a sass file and add the resulting css.
///
/// If `src` is `"somefile.sass"`, then that file will be copiled
/// with [rsass] (using the `Comressed` output style).
/// The result will be added as if if was an existing
/// `"somefile.css"` file.
///
/// While handling the scss input, rsass is extended with a
/// `static_name` sass function that takes a file name as given to
/// [`add_file()`][Self::add_file] (or simliar) and returns the
/// `name-hash.ext` filename that ructe creates for it.
/// Note that only files that are added to the `StaticFiles`
/// _before_ the call to `add_sass_files` are supported by the
/// `static_name` function.
///
/// This method is only available when ructe is built with the
/// "sass" feature.
#[cfg(feature = "sass")]
pub fn add_sass_file(&mut self, src: P) -> Result<&mut Self>
where
P: AsRef,
{
use rsass::css::CssString;
use rsass::input::CargoContext;
use rsass::output::{Format, Style};
use rsass::sass::{CallError, FormalArgs};
use rsass::value::Quotes;
use rsass::*;
use std::sync::Arc;
let format = Format {
style: Style::Compressed,
precision: 4,
};
let src = self.path_for(src);
let (context, scss) =
CargoContext::for_path(&src).map_err(rsass::Error::from)?;
let mut context = context.with_format(format);
let existing_statics = self.get_names().clone();
context.get_scope().define_function(
"static_name".into(),
sass::Function::builtin(
"",
&"static_name".into(),
FormalArgs::new(vec![("name".into(), None)]),
Arc::new(move |s| {
let name: String = s.get("name".into())?;
let rname = name.replace('-', "_").replace('.', "_");
existing_statics
.iter()
.find(|(n, _v)| *n == &rname)
.map(|(_n, v)| {
CssString::new(v.into(), Quotes::Double).into()
})
.ok_or_else(|| {
CallError::msg(format!(
"Static file {name:?} not found",
))
})
}),
),
);
let css = context.transform(scss)?;
self.add_file_data(&src.with_extension("css"), &css)
}
fn add_static(
&mut self,
path: &Path,
rust_name: &str,
url_name: &str,
content: &impl Display,
suffix: &str,
) -> Result<&mut Self> {
let mut rust_name =
rust_name.replace(|c: char| !c.is_alphanumeric(), "_");
if rust_name
.as_bytes()
.first()
.map(|c| c.is_ascii_digit())
.unwrap_or(true)
{
rust_name.insert(0, 'n');
}
writeln!(
self.src,
"\n/// From {path:?}\
\n#[allow(non_upper_case_globals)]\
\npub static {rust_name}: StaticFile = StaticFile {{\
\n content: {content},\
\n name: \"{url_name}\",\
\n{mime}\
}};",
path = path,
rust_name = rust_name,
url_name = url_name,
content = content,
mime = mime_arg(suffix),
)?;
self.names.insert(rust_name.clone(), url_name.into());
self.names_r.insert(url_name.into(), rust_name);
Ok(self)
}
/// Get a mapping of names, from without hash to with.
///
/// ````
/// # use ructe::{Result, Ructe, StaticFiles};
/// # use std::fs::create_dir_all;
/// # use std::path::PathBuf;
/// # use std::vec::Vec;
/// # fn main() -> Result<()> {
/// # let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target").join("test-tmp").join("get-names");
/// # create_dir_all(&p);
/// # let mut ructe = Ructe::new(p)?;
/// let mut statics = ructe.statics()?;
/// statics.add_file_data("black.css", b"body{color:black}\n");
/// statics.add_file_data("blue.css", b"body{color:blue}\n");
/// assert_eq!(
/// statics.get_names().iter()
/// .map(|(a, b)| format!("{} -> {}", a, b))
/// .collect::>(),
/// vec!["black_css -> black-r3rltVhW.css".to_string(),
/// "blue_css -> blue-GZGxfXag.css".to_string()],
/// );
/// # Ok(())
/// # }
/// ````
pub fn get_names(&self) -> &BTreeMap {
&self.names
}
}
impl Drop for StaticFiles {
/// Write the ending of the statics source code, declaring the
/// `STATICS` variable.
fn drop(&mut self) {
// Ignore a possible write failure, rather than a panic in drop.
let _ = writeln!(
self.src,
"\npub static STATICS: &[&StaticFile] \
= &[{}];",
self.names_r
.iter()
.map(|s| format!("&{}", s.1))
.format(", "),
);
let _ = super::write_if_changed(&self.src_path, &self.src);
}
}
struct FileContent<'a>(&'a Path);
impl<'a> Display for FileContent<'a> {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
write!(out, "include_bytes!({:?})", self.0)
}
}
struct ByteString<'a>(&'a [u8]);
impl<'a> Display for ByteString<'a> {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
out.write_str("b\"")?;
for byte in self.0 {
escape_default(*byte).fmt(out)?;
}
out.write_str("\"")
}
}
fn name_and_ext(path: &Path) -> Option<(&str, &str)> {
if let (Some(name), Some(ext)) = (path.file_name(), path.extension()) {
if let (Some(name), Some(ext)) = (name.to_str(), ext.to_str()) {
return Some((&name[..name.len() - ext.len() - 1], ext));
}
}
None
}
#[cfg(any(
all(feature = "mime03", feature = "http-types"),
all(feature = "mime02", feature = "http-types"),
all(feature = "mime02", feature = "mime03"),
))]
compile_error!(
r#"Only one of these features "http-types", "mime02" or "mime03" must be enabled at a time."#
);
/// A short and url-safe checksum string from string data.
fn checksum_slug(data: &[u8]) -> String {
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
BASE64_URL_SAFE_NO_PAD.encode(&md5::compute(data)[..6])
}
#[cfg(not(feature = "mime02"))]
#[cfg(not(feature = "mime03"))]
#[cfg(not(feature = "http-types"))]
fn mime_arg(_: &str) -> String {
"".to_string()
}
#[cfg(feature = "mime02")]
fn mime_arg(suffix: &str) -> String {
format!(" _mime: {:?},\n", mime_from_suffix(suffix))
}
#[cfg(feature = "mime02")]
fn mime_from_suffix(suffix: &str) -> &'static str {
match suffix.to_lowercase().as_ref() {
"bmp" => "image/bmp",
"css" => "text/css",
"eot" => "application/vnd.ms-fontobject",
"gif" => "image/gif",
"jpg" | "jpeg" => "image/jpeg",
"js" | "jsonp" => "application/javascript",
"json" => "application/json",
"png" => "image/png",
"svg" => "image/svg+xml",
"woff" => "font/woff",
"woff2" => "font/woff2",
_ => "application/octet-stream",
}
}
#[cfg(any(feature = "mime03", feature = "http-types"))]
fn mime_arg(suffix: &str) -> String {
format!(" mime: &mime::{},\n", mime_from_suffix(suffix))
}
#[cfg(feature = "mime03")]
fn mime_from_suffix(suffix: &str) -> &'static str {
// This is limited to the constants that is defined in mime 0.3.
match suffix.to_lowercase().as_ref() {
"bmp" => "IMAGE_BMP",
"css" => "TEXT_CSS",
"gif" => "IMAGE_GIF",
"jpg" | "jpeg" => "IMAGE_JPEG",
"js" | "jsonp" => "TEXT_JAVASCRIPT",
"json" => "APPLICATION_JSON",
"png" => "IMAGE_PNG",
"svg" => "IMAGE_SVG",
"woff" => "FONT_WOFF",
"woff2" => "FONT_WOFF",
_ => "APPLICATION_OCTET_STREAM",
}
}
#[cfg(feature = "http-types")]
fn mime_from_suffix(suffix: &str) -> &'static str {
match suffix.to_lowercase().as_ref() {
"css" => "CSS",
"html" | "htm" => "CSS",
"ico" => "ICO",
"jpg" | "jpeg" => "JPEG",
"js" | "jsonp" => "JAVASCRIPT",
"json" => "JSON",
"png" => "PNG",
"svg" => "SVG",
"txt" => "PLAIN",
"wasm" => "WASM",
"xml" => "XML",
_ => "mime::BYTE_STREAM",
}
}