Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<html>
<body data-order="" data-count="" data-ready-status="">
<script>
// Real project-generated font: moli-layout/tests/fixtures/moli-ahem.woff2.
const FONT_FIXTURE_SOURCE = "url(data:font/woff2;base64,d09GMgABAAAAAAKMAAoAAAAAEewAAAI9AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAARAqSGIlCATYCJAOBRguBRgAEIAWDDweHZhsaDBEVjE2QrxO4OaB11Uo4KkNFQmQIgNrHOq+V6mgnvY2fvSQ/uF+9N7PdNFuXpclV2mkpdFaAC3aRoEdFB3L8iVa3NRGchB7CA+kvmysBs3ExDyF+2La9iSrRM8x/YMEH4+YNnI6ADDhw44HnsPGNf2vg8Vh2oEcLbcza3qYp3gsXSOBToI2cQC+UhHthpAFIuk0EVfwGmbm3vt4jtOf5c3NBuwr0P4kB5kvBCdDgYzQftZAtCfcwRbHKu3S8ycdHAOfu8OUAmLjP+bEQEov+i78AFoAP+AEAxAAAAbQvPfKjw6g3kr+bmM88bAz4CAIItbXF3xu9/n2qfwCnW7usCnIKG0p4i7hd9+SA9QzCt8+eB2LUsMiFeojjkCtwessPXvTy47FKi+3KLnHVsltY375HzLDkOf6NhCkpi4iVsmpyVHYpuS67Fdw/4GE+R/a2N2xwfWXnu79JZ8nsauts62iP3v/+6UJH9FXrZ8WIPds2mEHrVuyYtGLNiW0L8jSL6zWd2nTMNs9acejIxsou80QHe+KtWi8h8LdYBGgJlS666aGXPvoZYJAhhhlhlDHGmbAhTCjjQiptrAMfhFGcpFlelFXdtF0/jNO8rNt+nNf9vB8AQjCCYjhBUjTDcrwgSrKiarphWrbjcb2+/jc6gLBCmFDGhVTaWBf+NANEmFDGhVTaWBf+tABEmFDGhVTaWBf+tAJEmFDGhVTaWBf+tAFEmFDGhVTaWBf+tANEmFDGhVTa2PClCtb3AQAAAA==)";
document.fonts.add(new FontFace('FixtureFace', FONT_FIXTURE_SOURCE));
const body = document.body;
const events = [];

Expand All @@ -10,7 +13,7 @@
document.fonts.onloadingerror = (event) => events.push(event.type);

document.fonts
.load('10px sans-serif')
.load('10px FixtureFace')
.then(() => document.fonts.ready)
.then(() => {
body.setAttribute('data-order', events.join(','));
Expand Down
13 changes: 8 additions & 5 deletions moli-core/tests/fixtures/runtime/document_fonts_events.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<html>
<body data-loading-a="" data-loading-b="" data-loadingdone="">
<script>
// Real project-generated font: moli-layout/tests/fixtures/moli-ahem.woff2.
const FONT_FIXTURE_SOURCE = "url(data:font/woff2;base64,d09GMgABAAAAAAKMAAoAAAAAEewAAAI9AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAARAqSGIlCATYCJAOBRguBRgAEIAWDDweHZhsaDBEVjE2QrxO4OaB11Uo4KkNFQmQIgNrHOq+V6mgnvY2fvSQ/uF+9N7PdNFuXpclV2mkpdFaAC3aRoEdFB3L8iVa3NRGchB7CA+kvmysBs3ExDyF+2La9iSrRM8x/YMEH4+YNnI6ADDhw44HnsPGNf2vg8Vh2oEcLbcza3qYp3gsXSOBToI2cQC+UhHthpAFIuk0EVfwGmbm3vt4jtOf5c3NBuwr0P4kB5kvBCdDgYzQftZAtCfcwRbHKu3S8ycdHAOfu8OUAmLjP+bEQEov+i78AFoAP+AEAxAAAAbQvPfKjw6g3kr+bmM88bAz4CAIItbXF3xu9/n2qfwCnW7usCnIKG0p4i7hd9+SA9QzCt8+eB2LUsMiFeojjkCtwessPXvTy47FKi+3KLnHVsltY375HzLDkOf6NhCkpi4iVsmpyVHYpuS67Fdw/4GE+R/a2N2xwfWXnu79JZ8nsauts62iP3v/+6UJH9FXrZ8WIPds2mEHrVuyYtGLNiW0L8jSL6zWd2nTMNs9acejIxsou80QHe+KtWi8h8LdYBGgJlS666aGXPvoZYJAhhhlhlDHGmbAhTCjjQiptrAMfhFGcpFlelFXdtF0/jNO8rNt+nNf9vB8AQjCCYjhBUjTDcrwgSrKiarphWrbjcb2+/jc6gLBCmFDGhVTaWBf+NANEmFDGhVTaWBf+tABEmFDGhVTaWBf+tAJEmFDGhVTaWBf+tAFEmFDGhVTaWBf+tANEmFDGhVTa2PClCtb3AQAAAA==)";
document.fonts.add(new FontFace('Roboto', FONT_FIXTURE_SOURCE));
let loadingA = 0;
let loadingB = 0;
let loadingDone = 0;
Expand All @@ -22,11 +25,11 @@
document.fonts.addEventListener('loading', onLoadingB);
document.fonts.addEventListener('loadingdone', onLoadingDone);
document.fonts.removeEventListener('loading', onLoadingA);
document.fonts.load('italic bold 16px Roboto');

document.body.setAttribute('data-loading-a', String(loadingA));
document.body.setAttribute('data-loading-b', String(loadingB));
document.body.setAttribute('data-loadingdone', String(loadingDone));
document.fonts.load('italic bold 16px Roboto').then(() => document.fonts.ready).then(() => {
document.body.setAttribute('data-loading-a', String(loadingA));
document.body.setAttribute('data-loading-b', String(loadingB));
document.body.setAttribute('data-loadingdone', String(loadingDone));
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
data-simple-error=""
>
<script>
// Real project-generated font: moli-layout/tests/fixtures/moli-ahem.woff2.
const FONT_FIXTURE_SOURCE = "url(data:font/woff2;base64,d09GMgABAAAAAAKMAAoAAAAAEewAAAI9AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAARAqSGIlCATYCJAOBRguBRgAEIAWDDweHZhsaDBEVjE2QrxO4OaB11Uo4KkNFQmQIgNrHOq+V6mgnvY2fvSQ/uF+9N7PdNFuXpclV2mkpdFaAC3aRoEdFB3L8iVa3NRGchB7CA+kvmysBs3ExDyF+2La9iSrRM8x/YMEH4+YNnI6ADDhw44HnsPGNf2vg8Vh2oEcLbcza3qYp3gsXSOBToI2cQC+UhHthpAFIuk0EVfwGmbm3vt4jtOf5c3NBuwr0P4kB5kvBCdDgYzQftZAtCfcwRbHKu3S8ycdHAOfu8OUAmLjP+bEQEov+i78AFoAP+AEAxAAAAbQvPfKjw6g3kr+bmM88bAz4CAIItbXF3xu9/n2qfwCnW7usCnIKG0p4i7hd9+SA9QzCt8+eB2LUsMiFeojjkCtwessPXvTy47FKi+3KLnHVsltY375HzLDkOf6NhCkpi4iVsmpyVHYpuS67Fdw/4GE+R/a2N2xwfWXnu79JZ8nsauts62iP3v/+6UJH9FXrZ8WIPds2mEHrVuyYtGLNiW0L8jSL6zWd2nTMNs9acejIxsou80QHe+KtWi8h8LdYBGgJlS666aGXPvoZYJAhhhlhlDHGmbAhTCjjQiptrAMfhFGcpFlelFXdtF0/jNO8rNt+nNf9vB8AQjCCYjhBUjTDcrwgSrKiarphWrbjcb2+/jc6gLBCmFDGhVTaWBf+NANEmFDGhVTaWBf+tABEmFDGhVTaWBf+tAJEmFDGhVTaWBf+tAFEmFDGhVTaWBf+tANEmFDGhVTa2PClCtb3AQAAAA==)";
document.fonts.add(new FontFace('Roboto', FONT_FIXTURE_SOURCE));
const body = document.body;

const fontOrder = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
data-css-wide-keyword-prefixed=""
>
<script>
// Real project-generated font: moli-layout/tests/fixtures/moli-ahem.woff2.
const FONT_FIXTURE_SOURCE = "url(data:font/woff2;base64,d09GMgABAAAAAAKMAAoAAAAAEewAAAI9AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAARAqSGIlCATYCJAOBRguBRgAEIAWDDweHZhsaDBEVjE2QrxO4OaB11Uo4KkNFQmQIgNrHOq+V6mgnvY2fvSQ/uF+9N7PdNFuXpclV2mkpdFaAC3aRoEdFB3L8iVa3NRGchB7CA+kvmysBs3ExDyF+2La9iSrRM8x/YMEH4+YNnI6ADDhw44HnsPGNf2vg8Vh2oEcLbcza3qYp3gsXSOBToI2cQC+UhHthpAFIuk0EVfwGmbm3vt4jtOf5c3NBuwr0P4kB5kvBCdDgYzQftZAtCfcwRbHKu3S8ycdHAOfu8OUAmLjP+bEQEov+i78AFoAP+AEAxAAAAbQvPfKjw6g3kr+bmM88bAz4CAIItbXF3xu9/n2qfwCnW7usCnIKG0p4i7hd9+SA9QzCt8+eB2LUsMiFeojjkCtwessPXvTy47FKi+3KLnHVsltY375HzLDkOf6NhCkpi4iVsmpyVHYpuS67Fdw/4GE+R/a2N2xwfWXnu79JZ8nsauts62iP3v/+6UJH9FXrZ8WIPds2mEHrVuyYtGLNiW0L8jSL6zWd2nTMNs9acejIxsou80QHe+KtWi8h8LdYBGgJlS666aGXPvoZYJAhhhlhlDHGmbAhTCjjQiptrAMfhFGcpFlelFXdtF0/jNO8rNt+nNf9vB8AQjCCYjhBUjTDcrwgSrKiarphWrbjcb2+/jc6gLBCmFDGhVTaWBf+NANEmFDGhVTaWBf+tABEmFDGhVTaWBf+tAJEmFDGhVTaWBf+tAFEmFDGhVTaWBf+tANEmFDGhVTa2PClCtb3AQAAAA==)";
document.fonts.add(new FontFace('FixtureFace', FONT_FIXTURE_SOURCE));
(async () => {
const body = document.body;
const ready1 = document.fonts.ready;
Expand Down
149 changes: 149 additions & 0 deletions moli-css-parse/src/font_face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,121 @@ use crate::unquote_css_string;

pub use style::moli_font_face::{CssFontFace, normalize_font_face_src, parse_font_faces};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CssFontSource {
Local(String),
Url(String),
}

/// Parse through Stylo first; the token walk only extracts validated sources.
pub fn parse_font_face_sources(source: &str) -> Option<Vec<CssFontSource>> {
let normalized = normalize_font_face_src(source)?;
let mut input = ParserInput::new(&normalized);
let mut parser = Parser::new(&mut input);
parser
.parse_comma_separated(|parser| {
let name = parser.expect_function()?.clone();
let source = parser.parse_nested_block(|nested| {
if name.eq_ignore_ascii_case("local") {
let mut parts = Vec::new();
while !nested.is_exhausted() {
parts.push(nested.expect_ident_or_string()?.to_string());
}
Ok(CssFontSource::Local(parts.join(" ")))
} else if name.eq_ignore_ascii_case("url") {
Ok(CssFontSource::Url(nested.expect_string()?.to_string()))
} else {
Err(nested.new_custom_error(()))
}
})?;
let mut supported = true;
while !parser.is_exhausted() {
let kind = parser.expect_function()?.clone();
parser.parse_nested_block(|nested| {
let mut values = Vec::new();
while !nested.is_exhausted() {
if nested.try_parse(|p| p.expect_comma()).is_ok() {
continue;
}
values.push(nested.expect_ident_or_string()?.to_ascii_lowercase());
}
supported &= kind.eq_ignore_ascii_case("format")
&& values.iter().all(|v| {
matches!(
v.as_str(),
"woff" | "woff2" | "truetype" | "opentype" | "collection"
)
});
Ok::<_, cssparser::ParseError<'_, ()>>(())
})?;
}
Ok(supported.then_some(source))
})
.ok()
.map(|sources| sources.into_iter().flatten().collect())
}

#[derive(Clone, Debug)]
pub struct CssFontShorthand {
pub family_css: String,
pub families: Vec<String>,
pub size: String,
pub style: String,
pub weight: String,
pub stretch: String,
}

/// Shares the CSS font grammar with style declarations, including quoted
/// multi-word families and fallback lists. No guessed last-whitespace token.
pub fn parse_font_shorthand(value: &str) -> Option<CssFontShorthand> {
use style::{
properties::{PropertyId, parse_property_declaration_list},
stylesheets::CssRuleType,
};
use style_traits::CssStringWriter;
if crate::escape_top_level_semicolons(value) != value
|| crate::split_important_priority(value).1
{
return None;
}
crate::stylo_stylesheet::with_descriptor_declaration_context(CssRuleType::Style, |context| {
let text = format!("font: {value}");
let mut input = ParserInput::new(&text);
let block = parse_property_declaration_list(context, &mut Parser::new(&mut input), &[]);
let property = |name: &str| -> Option<String> {
let mut out = CssStringWriter::new();
block
.property_value_to_css(&PropertyId::parse(name, context).ok()?, &mut out)
.ok()?;
(!out.is_empty()).then(|| out.to_string())
};
let family_css = property("font-family")?;
let size = property("font-size")?;
if is_css_wide_keyword(&size) || size.contains("var(") {
return None;
}
let mut input = ParserInput::new(&family_css);
let families = Parser::new(&mut input)
.parse_comma_separated(|p| {
let mut parts = Vec::new();
while !p.is_exhausted() {
parts.push(p.expect_ident_or_string()?.to_string());
}
Ok::<_, cssparser::ParseError<'_, ()>>(parts.join(" "))
})
.ok()?;
drop(input);
Some(CssFontShorthand {
family_css,
families,
size,
style: property("font-style")?,
weight: property("font-weight")?,
stretch: property("font-stretch")?,
})
})
}

pub fn font_load_query_contains_css_wide_keyword(query: &str) -> bool {
let mut input = ParserInput::new(query);
let mut input = Parser::new(&mut input);
Expand Down Expand Up @@ -50,6 +165,40 @@ fn is_css_wide_keyword(value: &str) -> bool {

#[cfg(test)]
mod tests {
#[test]
fn font_sources_preserve_local_names_and_supported_url_fallback_order() {
use super::*;
assert_eq!(
parse_font_face_sources(
"local(Missing Font), url(bad) format('unsupported'), url(good.woff2) format('woff2')"
),
Some(vec![
CssFontSource::Local("Missing Font".into()),
CssFontSource::Url("good.woff2".into())
])
);
assert!(parse_font_face_sources("not a source").is_none());
}

#[test]
fn font_shorthand_uses_stylo_validation_and_keeps_all_families() {
let font =
super::parse_font_shorthand("italic bold 10.5px/1.2 \"A B\", Another Face, serif")
.unwrap();
assert_eq!(font.families, ["A B", "Another Face", "serif"]);
assert_eq!(font.size, "10.5px");
assert_eq!(font.style, "italic");
for invalid in [
"",
"12px",
"not a font",
"inherit",
"12px serif; color:red",
"12px serif !important",
] {
assert!(super::parse_font_shorthand(invalid).is_none(), "{invalid}");
}
}
use super::{
font_load_query_contains_css_wide_keyword, font_load_query_family, normalize_font_face_src,
parse_font_faces,
Expand Down
5 changes: 3 additions & 2 deletions moli-css-parse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ pub use color::{
};
pub use declaration::{CssDeclaration, DeclarationParseOptions, parse_declaration_list};
pub use font_face::{
CssFontFace, font_load_query_contains_css_wide_keyword, font_load_query_family,
normalize_font_face_src, parse_font_faces,
CssFontFace, CssFontShorthand, CssFontSource, font_load_query_contains_css_wide_keyword,
font_load_query_family, normalize_font_face_src, parse_font_face_sources, parse_font_faces,
parse_font_shorthand,
};
pub use math::{
ContainerQueryLengthContext, CssNumericContext, CssNumericKind, CssNumericValue, UnitlessAngle,
Expand Down
2 changes: 1 addition & 1 deletion moli-css-parse/src/stylo_stylesheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ fn declaration_block_css_text(block: &PropertyDeclarationBlock) -> String {
css_text.trim_end().to_owned()
}

fn with_descriptor_declaration_context<R>(
pub(crate) fn with_descriptor_declaration_context<R>(
rule_type: CssRuleType,
f: impl FnOnce(&ParserContext) -> Option<R>,
) -> Option<R> {
Expand Down
1 change: 1 addition & 0 deletions moli-layout/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ euclid = "=0.22.14"
kurbo = "=0.13.1"
moli-image = { path = "../moli-image" }
parley = "=0.11.1"
read-fonts = "=0.41.0"
parking_lot = "0.12"
stylo = "0.20"
stylo_taffy = { git = "https://github.com/DioxusLabs/blitz", rev = "5081c65811a4396f5a99b2e0aca542a4a4a6606f", default-features = false, features = ["std", "block", "flexbox", "grid", "floats"] }
Expand Down
2 changes: 1 addition & 1 deletion moli-layout/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pub use style::{
LayoutPosition, ResolvedLayoutStyle,
};
pub use text::{
DocumentLayoutServices, SystemFontPolicy, WebFontFace, WebFontRegistration,
DocumentLayoutServices, FontFaceData, SystemFontPolicy, WebFontFace, WebFontRegistration,
WebFontRegistrationError, WebFontRegistrationOutcome, WebFontStyle, WebFontUnicodeRange,
};
pub use world::{
Expand Down
18 changes: 18 additions & 0 deletions moli-layout/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ use thiserror::Error;
use crate::stylo_to_parley::TextBrush;
use crate::system_fonts::SystemFontFamilyResolver;

mod font_source;
pub use font_source::FontFaceData;

pub(crate) struct ParleyDocumentServices {
pub(crate) font_context: FontContext,
pub(crate) layout_context: LayoutContext<TextBrush>,
Expand Down Expand Up @@ -674,6 +677,7 @@ pub struct DocumentLayoutServices {
// FontContext and LayoutContext are both large. Keep them off the stack so
// embedding this sidecar in ScriptVm does not inflate every VM frame.
parley: Option<Box<ParleyDocumentServices>>,
local_font_sources: Option<Box<font_source::LocalFontSources>>,
system_font_policy: SystemFontPolicy,
web_fonts: BTreeMap<String, RegisteredWebFont>,
pub(crate) text_layout_passes: u64,
Expand All @@ -690,6 +694,7 @@ impl DocumentLayoutServices {
pub const fn new() -> Self {
Self {
parley: None,
local_font_sources: None,
system_font_policy: SystemFontPolicy::Enabled,
web_fonts: BTreeMap::new(),
text_layout_passes: 0,
Expand All @@ -700,6 +705,7 @@ impl DocumentLayoutServices {
pub const fn with_system_font_policy(system_font_policy: SystemFontPolicy) -> Self {
Self {
parley: None,
local_font_sources: None,
system_font_policy,
web_fonts: BTreeMap::new(),
text_layout_passes: 0,
Expand Down Expand Up @@ -775,6 +781,18 @@ impl DocumentLayoutServices {
true
}

/// Resolves a CSS local() source by its actual full or PostScript name.
/// This never substitutes a generic font or a document web-font alias.
pub fn local_font_source(&mut self, name: &str) -> Option<FontFaceData> {
self.local_font_sources
.get_or_insert_with(|| {
Box::new(font_source::LocalFontSources::new(
self.system_font_policy.is_enabled(),
))
})
.find(name)
}

pub(crate) fn parley_mut(&mut self) -> &mut ParleyDocumentServices {
if self.parley.is_none() {
self.parley = Some(Box::new(build_parley_services(
Expand Down
Loading
Loading