use crate::{
gather_css::{parse_font_families, ParsedCssRule},
webroot::RelaWebroot,
RewriteContext,
};
use anyhow::{bail, Result};
use arcstr::ArcStr;
use lightningcss::{
declaration::DeclarationBlock,
printer::PrinterOptions,
properties::{
font::{AbsoluteFontWeight, FontFamily, FontWeight as CssFontWeight},
Property,
},
rules::{
font_face::{
FontFaceProperty, FontFaceRule, FontFormat, FontStyle as CssFontStyle, Source,
UnicodeRange, UrlSource,
},
CssRule, CssRuleList, Location,
},
stylesheet::{ParserOptions, StyleSheet},
traits::{ToCss, Zero},
values::{angle::Angle, size::Size2D, url::Url},
};
use mkwebfont_common::{
hashing::WyHashSet,
paths::{get_relative_from, is_superpath},
};
use mkwebfont_fontops::font_info::FontStyle;
use std::{borrow::Cow, sync::Arc};
use tracing::{debug, info};
const DEFAULT_LOC: Location = Location { source_index: 0, line: 0, column: 0 };
const DEFAULT_LOC_CSS: lightningcss::dependencies::Location =
lightningcss::dependencies::Location { line: 0, column: 0 };
fn printer() -> PrinterOptions<'static> {
let mut options = PrinterOptions::default();
options.minify = false;
options
}
fn generate_font_face_stylesheet<'a, 'b>(
ctx: &RewriteContext,
store_uri: &str,
used_stacks: Option<&WyHashSet<Arc<[ArcStr]>>>,
fallback_needed: bool,
) -> StyleSheet<'a, 'b> {
let mut sheet = StyleSheet::new(vec![], CssRuleList(vec![]), ParserOptions::default());
let store_prefix = if store_uri.is_empty() {
String::new()
} else {
format!("{store_uri}/")
};
'font_loop: for font in &ctx.webfonts {
if font.font_family() == &ctx.fallback_font_name {
if !fallback_needed {
continue;
}
if let Some(used_stacks) = &used_stacks {
'outer: {
for stack in &**used_stacks {
if ctx.fallback_info.contains_key(stack) {
break 'outer;
}
}
continue 'font_loop;
}
}
} else {
if let Some(used_stacks) = &used_stacks {
'outer: {
for stack in &**used_stacks {
if stack
.iter()
.any(|x| x.as_str() == &font.font_family().to_lowercase())
{
break 'outer;
}
}
continue 'font_loop;
}
}
}
let weight_range = font.weight_range();
let weight_low = *weight_range.start();
let weight_high = *weight_range.end();
let weight_range = Size2D(
CssFontWeight::Absolute(AbsoluteFontWeight::Weight(weight_low as f32)),
CssFontWeight::Absolute(AbsoluteFontWeight::Weight(weight_high as f32)),
);
for subset in font.subsets() {
let mut font_face = FontFaceRule { properties: vec![], loc: DEFAULT_LOC };
font_face
.properties
.push(FontFaceProperty::FontFamily(FontFamily::FamilyName(
font.font_family().to_string().into(),
)));
font_face.properties.push(FontFaceProperty::FontStyle(
match font.parsed_font_style() {
FontStyle::Regular => CssFontStyle::Normal,
FontStyle::Italic => CssFontStyle::Italic,
FontStyle::Oblique => {
CssFontStyle::Oblique(Size2D(Angle::zero(), Angle::zero()))
}
},
));
font_face
.properties
.push(FontFaceProperty::FontWeight(weight_range.clone()));
font_face.properties.push(FontFaceProperty::UnicodeRange(
subset
.unicode_ranges()
.into_iter()
.map(|r| UnicodeRange { start: *r.start(), end: *r.end() })
.collect(),
));
font_face
.properties
.push(FontFaceProperty::Source(vec![Source::Url(UrlSource {
url: Url {
url: format!("{store_prefix}{}", subset.woff2_file_name()).into(),
loc: DEFAULT_LOC_CSS,
},
format: Some(FontFormat::WOFF2),
tech: vec![],
})]));
sheet.rules.0.push(CssRule::FontFace(font_face));
}
}
sheet
}
fn rewrite_properties_for_fallback(
ctx: &RewriteContext,
properties: &mut DeclarationBlock,
) -> bool {
let mut rewritten = false;
for property in properties
.declarations
.iter_mut()
.chain(properties.important_declarations.iter_mut())
{
match property {
Property::FontFamily(family) => {
let ParsedCssRule::Override(families) = parse_font_families(&family) else {
continue;
};
let init_len = family.len();
family.retain(|x| matches!(x, FontFamily::FamilyName(_)));
if init_len != family.len() {
rewritten = true;
}
if ctx.fallback_info.contains_key(&families) {
family.push(FontFamily::FamilyName(ctx.fallback_font_name.clone().into()));
rewritten = true;
}
}
_ => {}
}
}
rewritten
}
fn rewrite_for_fallback(ctx: &RewriteContext, css: &mut [CssRule]) -> bool {
let mut rewritten = false;
for rule in css {
match rule {
CssRule::Media(media_query) => {
rewritten |= rewrite_for_fallback(ctx, &mut media_query.rules.0);
}
CssRule::Style(rule) => {
rewritten |= rewrite_properties_for_fallback(ctx, &mut rule.declarations);
}
_ => {}
}
}
rewritten
}
fn add_font_faces(
css: &mut StyleSheet,
ctx: &RewriteContext,
store_url: &str,
used_stacks: Option<&WyHashSet<Arc<[ArcStr]>>>,
fallback_needed: bool,
) {
let sheet = generate_font_face_stylesheet(ctx, store_url, used_stacks, fallback_needed);
css.rules.0.extend(sheet.rules.0);
}
fn find_store_uri<'a>(ctx: &'a RewriteContext, root: &RelaWebroot) -> Result<Cow<'a, str>> {
if let Some(uri) = &ctx.store_uri {
Ok(Cow::Borrowed(uri.as_str()))
} else {
if !is_superpath(root.root().root(), &ctx.store_path)? {
bail!(
"Store path must be subdirectory of the webroot, \
or `--store-uri` must be provided."
);
}
Ok(Cow::Owned(get_relative_from(&root.file_name(), &ctx.store_path)?))
}
}
fn rewrite_css(
ctx: &RewriteContext,
root: &RelaWebroot,
append_fonts: bool,
used_stacks: Option<&WyHashSet<Arc<[ArcStr]>>>,
fallback_needed: bool,
) -> Result<()> {
let data = std::fs::read_to_string(root.file_name())?;
let mut sheet =
StyleSheet::parse(&data, ParserOptions::default()).map_err(|x| x.into_owned())?;
let mut rewritten = rewrite_for_fallback(ctx, &mut sheet.rules.0);
if append_fonts {
let store_uri = if let Some(uri) = &ctx.store_uri {
Cow::Borrowed(uri.as_str())
} else {
Cow::Owned(get_relative_from(&root.file_name(), &ctx.store_path)?)
};
debug!(
"(Appending fonts) Store URI for {} -> {}: {store_uri}",
root.file_name().display(),
ctx.store_path.display(),
);
add_font_faces(&mut sheet, ctx, &find_store_uri(ctx, root)?, used_stacks, fallback_needed);
rewritten = true;
}
if rewritten {
info!("Writing modified CSS to {}...", root.file_name().display());
std::fs::write(root.file_name(), sheet.to_css(printer())?.code)?;
} else {
debug!("CSS does not need rewriting.");
}
Ok(())
}
fn generate_css(
ctx: &RewriteContext,
root: &RelaWebroot,
used_stacks: Option<&WyHashSet<Arc<[ArcStr]>>>,
fallback_needed: bool,
) -> Result<()> {
let sheet = generate_font_face_stylesheet(
ctx,
&find_store_uri(ctx, root)?,
used_stacks,
fallback_needed,
);
info!("Writing @font-face CSS to {}...", root.file_name().display());
std::fs::write(root.file_name(), sheet.to_css(printer())?.code)?;
Ok(())
}
pub fn generate_font_css(ctx: &RewriteContext) -> Result<String> {
let Some(store_uri) = &ctx.store_uri else {
bail!("`--store_uri` is required for generating detached font CSS.")
};
let sheet = generate_font_face_stylesheet(ctx, &store_uri, None, false);
Ok(sheet.to_css(printer())?.code)
}
pub fn rewrite_style_attr(ctx: &RewriteContext, style: &str) -> Result<Option<String>> {
match DeclarationBlock::parse_string(style, ParserOptions::default()) {
Ok(mut block) => {
if rewrite_properties_for_fallback(ctx, &mut block) {
Ok(Some(block.to_css_string(printer())?))
} else {
Ok(None)
}
}
Err(_) => Ok(None),
}
}
pub fn rewrite_style_tag(ctx: &RewriteContext, style: &str) -> Result<Option<String>> {
let mut sheet =
StyleSheet::parse(style, ParserOptions::default()).map_err(|x| x.into_owned())?;
if rewrite_for_fallback(ctx, &mut sheet.rules.0) {
Ok(Some(sheet.to_css(printer())?.code))
} else {
Ok(None)
}
}
pub fn process_css_path(
ctx: &RewriteContext,
root: &RelaWebroot,
append_fonts: bool,
used_stacks: Option<&WyHashSet<Arc<[ArcStr]>>>,
) -> Result<()> {
if !root.file_name().exists() {
if !append_fonts {
Ok(())
} else {
generate_css(ctx, root, used_stacks, true)
}
} else {
rewrite_css(ctx, root, append_fonts, used_stacks, true)
}
}