From fc8499110ce32f64bc7d585237c7c0df4c26fa99 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 15 Jul 2026 16:24:57 +0800 Subject: [PATCH] fix(fonts): load string sources on demand --- .../wpt-cross-current/passed-cases.txt | 1 + .../chrome_fontfaceset_events_subset.html | 3 +- .../runtime/document_fonts_events.html | 1 + ...local_event_target_callback_exception.html | 1 + .../runtime/servo_fontfaceset_load_ready.html | 1 + moli-layout/src/style.rs | 20 +-- moli-renderer-v8/src/context_bootstrap.rs | 4 +- .../context_bootstrap/css_fontface_runtime.rs | 2 + .../css_fontface_runtime/events.rs | 11 +- .../css_fontface_runtime/font_face.rs | 141 +++++++++++++++++- .../css_fontface_runtime/font_face_set.rs | 5 +- .../font_face_set/loading.rs | 27 +++- .../src/document_runtime/mutation_commands.rs | 17 +++ .../src/native_bridge/document.rs | 3 +- .../src/native_bridge/document/css_state.rs | 1 + .../document/css_state/font_faces.rs | 63 ++++++++ .../src/script_vm/tests/browser_api/misc.rs | 74 ++++++++- .../css-font-loading/fontface-basic.html | 5 +- .../wpt/ported/idl/fontface-idlharness.html | 2 +- 19 files changed, 339 insertions(+), 43 deletions(-) diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 5fc102d56e..50beb6742b 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -1577,6 +1577,7 @@ css/css-flexbox/webkit-box-vertical-writing-mode.html css/css-flexbox/whitespace-in-flexitem-001.html css/css-flexbox/zero-content-size-with-scrollbar-crash.html css/css-font-loading/empty-family-load.html +css/css-font-loading/font-face-reject.html css/css-font-loading/fontface-font-variation-settings-persisted-js-api.html css/css-font-loading/fontface-fonts-loading.html css/css-font-loading/fontface-invalid-arraybuffer.html diff --git a/moli-core/tests/fixtures/runtime/chrome_fontfaceset_events_subset.html b/moli-core/tests/fixtures/runtime/chrome_fontfaceset_events_subset.html index 4350f76871..5a28ef20bc 100644 --- a/moli-core/tests/fixtures/runtime/chrome_fontfaceset_events_subset.html +++ b/moli-core/tests/fixtures/runtime/chrome_fontfaceset_events_subset.html @@ -8,9 +8,10 @@ document.fonts.onloading = (event) => events.push(event.type); document.fonts.onloadingdone = (event) => events.push(event.type); document.fonts.onloadingerror = (event) => events.push(event.type); + document.fonts.add(new FontFace('FixtureFace', 'url(fixture.woff2)')); document.fonts - .load('10px sans-serif') + .load('10px FixtureFace') .then(() => document.fonts.ready) .then(() => { body.setAttribute('data-order', events.join(',')); diff --git a/moli-core/tests/fixtures/runtime/document_fonts_events.html b/moli-core/tests/fixtures/runtime/document_fonts_events.html index b7a7b4e3fb..e662a62625 100644 --- a/moli-core/tests/fixtures/runtime/document_fonts_events.html +++ b/moli-core/tests/fixtures/runtime/document_fonts_events.html @@ -22,6 +22,7 @@ document.fonts.addEventListener('loading', onLoadingB); document.fonts.addEventListener('loadingdone', onLoadingDone); document.fonts.removeEventListener('loading', onLoadingA); + document.fonts.add(new FontFace('Roboto', 'url(roboto.woff2)')); document.fonts.load('italic bold 16px Roboto'); document.body.setAttribute('data-loading-a', String(loadingA)); diff --git a/moli-core/tests/fixtures/runtime/local_event_target_callback_exception.html b/moli-core/tests/fixtures/runtime/local_event_target_callback_exception.html index 91ba69495f..c494c3a0b1 100644 --- a/moli-core/tests/fixtures/runtime/local_event_target_callback_exception.html +++ b/moli-core/tests/fixtures/runtime/local_event_target_callback_exception.html @@ -27,6 +27,7 @@ throw new Error("font handler boom"); }; try { + document.fonts.add(new FontFace("Roboto", "url(roboto.woff2)")); document.fonts.load("italic bold 16px Roboto"); body.setAttribute("data-font-continued", "true"); } catch (error) { diff --git a/moli-core/tests/fixtures/runtime/servo_fontfaceset_load_ready.html b/moli-core/tests/fixtures/runtime/servo_fontfaceset_load_ready.html index 882d86acf4..ffe0657ca4 100644 --- a/moli-core/tests/fixtures/runtime/servo_fontfaceset_load_ready.html +++ b/moli-core/tests/fixtures/runtime/servo_fontfaceset_load_ready.html @@ -14,6 +14,7 @@ const ready1 = document.fonts.ready; body.setAttribute('data-ready-stable', String(ready1 === document.fonts.ready)); + document.fonts.add(new FontFace('FixtureFace', 'url(fixture.woff2)')); const loadPromise = document.fonts.load('10px FixtureFace'); const ready2 = document.fonts.ready; body.setAttribute('data-ready-replaced', String(ready1 !== ready2)); diff --git a/moli-layout/src/style.rs b/moli-layout/src/style.rs index 478e3bb4fc..1407c6d687 100644 --- a/moli-layout/src/style.rs +++ b/moli-layout/src/style.rs @@ -2459,16 +2459,16 @@ fn stylo_list_marker_type(computed: &ComputedValues) -> LayoutListMarkerType { _ => LayoutListMarkerType::Fallback, }, CounterStyle::String(value) => LayoutListMarkerType::String(Arc::from(value.as_ref())), - CounterStyle::Symbols { symbols, .. } => LayoutListMarkerType::Symbols( - symbols - .0 - .iter() - .map(|symbol| match symbol { - Symbol::String(value) => Arc::from(value.as_ref()), - Symbol::Ident(value) => Arc::from(value.0.as_ref()), - }) - .collect(), - ), + CounterStyle::Symbols { symbols, .. } => { + let mut text_symbols = Vec::with_capacity(symbols.0.len()); + for symbol in symbols.0.iter() { + match symbol { + Symbol::String(value) => text_symbols.push(Arc::from(value.as_ref())), + Symbol::Ident(value) => text_symbols.push(Arc::from(value.0.as_ref())), + } + } + LayoutListMarkerType::Symbols(text_symbols) + } } } diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index cec044df5e..c7bb9409c7 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -137,7 +137,9 @@ pub(crate) use crypto::{ CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, WebCryptoRejection, WebCryptoTaskResult, crypto_key_clone_payload_from_object, crypto_key_object_from_clone_payload, }; -pub(crate) use css_fontface_runtime::{new_font_face_set, rebuild_font_face_set_faces}; +pub(crate) use css_fontface_runtime::{ + load_font_faces_for_family, new_font_face_set, rebuild_font_face_set_faces, +}; pub(crate) use location_navigation::{ LocationNavigationKind, dispatch_top_level_form_navigation_event, dispatch_top_level_navigation_event_with_source_element, meta_refresh_navigation_kind, diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs index 1e9f95a552..6712d39b9a 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs @@ -25,6 +25,7 @@ pub(super) const FONT_FACE_LINE_GAP_OVERRIDE_SLOT: &str = "__moliFontFaceLineGap pub(super) const FONT_FACE_SIZE_ADJUST_SLOT: &str = "__moliFontFaceSizeAdjust"; pub(super) const FONT_FACE_STATUS_SLOT: &str = "__moliFontFaceStatus"; pub(super) const FONT_FACE_LOADED_SLOT: &str = "__moliFontFaceLoaded"; +pub(super) const FONT_FACE_LOADED_RESOLVER_SLOT: &str = "__moliFontFaceLoadedResolver"; pub(super) const FONT_FACE_SET_OWNERS_SLOT: &str = "__moliFontFaceSetOwners"; pub(super) const FONT_FACE_LOAD_NOTIFICATION_SENT_SLOT: &str = "__moliFontFaceLoadNotificationSent"; pub(super) const FONT_FACE_SET_MANUAL_FACES_SLOT: &str = "__moliFontFaceSetManualFaces"; @@ -44,6 +45,7 @@ pub(in crate::context_bootstrap) use events::{ initialize_font_face_set_load_event, install_font_face_set_event_handler_accessors, install_font_face_set_load_event_template_accessors, }; +pub(crate) use font_face::load_font_faces_for_family; pub(super) use font_face::{ font_face_constructor_callback, font_face_load_callback, install_font_face_template_accessors, }; diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/events.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/events.rs index 00aa56a8c8..896043b063 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/events.rs @@ -304,7 +304,7 @@ pub(super) fn notify_font_face_set_owners_of_load<'s>( } let loaded_faces = v8::Array::new(scope, 1); let _ = loaded_faces.set_index(scope, 0, face.into()); - let completion_event_type = if font_face_load_failed(scope, face) { + let completion_event_type = if super::font_face::font_face_load_failed(scope, face) { "loadingerror" } else { "loadingdone" @@ -318,12 +318,3 @@ pub(super) fn notify_font_face_set_owners_of_load<'s>( dispatch_font_face_set_event(scope, owner, completion_event_type, Some(loaded_faces)); } } - -fn font_face_load_failed<'s>( - scope: &mut v8::PinScope<'s, '_>, - face: v8::Local<'s, v8::Object>, -) -> bool { - get_private_value(scope, face, FONT_FACE_STATUS_SLOT) - .and_then(|value| v8::Local::::try_from(value).ok()) - .is_some_and(|status| status.to_rust_string_lossy(scope) == "error") -} diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs index 943a964f78..3b9699cdb8 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs @@ -44,6 +44,8 @@ struct FontFaceObjectDeclaration<'s> { status: &'static str, #[webapi(slot = FONT_FACE_LOADED_SLOT)] loaded: Option>, + #[webapi(slot = FONT_FACE_LOADED_RESOLVER_SLOT)] + loaded_resolver: Option>, #[webapi(slot = FONT_FACE_SET_OWNERS_SLOT, constructor_default = Vec::new())] owner_sets: Vec>, #[webapi(slot = FONT_FACE_LOAD_NOTIFICATION_SENT_SLOT, constructor_default = false)] @@ -305,7 +307,7 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>( size_adjust, ] = descriptors; let this = args.this(); - let (source, status, loaded) = if invalid_descriptors { + let (source, status, loaded, loaded_resolver) = if invalid_descriptors { let source = match parsed.source { FontFaceConstructorSource::Css(source) => source, FontFaceConstructorSource::Binary(_) => String::new(), @@ -315,18 +317,30 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>( "SyntaxError", "Invalid FontFace descriptor.", ); - (source, "error", Some(loaded)) + (source, "error", Some(loaded), None) } else { match parsed.source { + FontFaceConstructorSource::Css(source) + if moli_css_parse::normalize_font_face_src(&source).is_some() => + { + let resolver = v8::PromiseResolver::new(scope) + .expect("FontFace loaded promise resolver should allocate"); + let loaded = resolver.get_promise(scope); + (source, "unloaded", Some(loaded), Some(resolver)) + } FontFaceConstructorSource::Css(source) => { - let loaded = resolved_promise(scope, this.into()); - (source, "loaded", loaded) + let loaded = super::query::make_rejected_dom_exception_promise( + scope, + "SyntaxError", + "Invalid FontFace source descriptor.", + ); + (source, "error", Some(loaded), None) } FontFaceConstructorSource::Binary(bytes) if moli_web_mime::sniff_font_mime_type(&bytes).is_some() => { let loaded = resolved_promise(scope, this.into()); - (String::new(), "loaded", loaded) + (String::new(), "loaded", loaded, None) } FontFaceConstructorSource::Binary(_) => { let loaded = super::query::make_rejected_dom_exception_promise( @@ -334,7 +348,7 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>( "SyntaxError", "Invalid font data in ArrayBuffer.", ); - (String::new(), "error", Some(loaded)) + (String::new(), "error", Some(loaded), None) } } }; @@ -355,6 +369,7 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>( size_adjust, status, loaded, + loaded_resolver, ) .initialize(scope, this) .expect("FontFace declaration should initialize object"); @@ -395,8 +410,7 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>( mut rv: v8::ReturnValue<'s, v8::Value>, ) { let this = args.this(); - super::events::notify_font_face_set_owners_of_load(scope, this); - if let Some(loaded) = font_face_slot_value(scope, this, FONT_FACE_LOADED_SLOT) { + if let Some(loaded) = load_font_face(scope, this) { rv.set(loaded); return; } @@ -406,6 +420,117 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>( } } +pub(super) fn load_font_face<'s>( + scope: &mut v8::PinScope<'s, '_>, + face: v8::Local<'s, v8::Object>, +) -> Option> { + let loaded = font_face_slot_value(scope, face, FONT_FACE_LOADED_SLOT); + if font_face_status(scope, face).as_deref() != Some("unloaded") { + super::events::notify_font_face_set_owners_of_load(scope, face); + return loaded; + } + + set_font_face_status(scope, face, "loading"); + let source = font_face_string_slot(scope, face, FONT_FACE_SOURCE_SLOT).unwrap_or_default(); + let succeeds = font_face_css_source_has_url(&source); + if succeeds { + set_font_face_status(scope, face, "loaded"); + } else { + set_font_face_status(scope, face, "error"); + } + + if let Some(resolver) = font_face_loaded_resolver(scope, face) { + if succeeds { + let _ = resolver.resolve(scope, face.into()); + } else { + let exception = crate::context_bootstrap::new_dom_exception_value( + scope, + "No source in the FontFace src list could be loaded.", + "NetworkError", + ); + let _ = resolver.reject(scope, exception); + } + } + super::events::notify_font_face_set_owners_of_load(scope, face); + loaded +} + +pub(super) fn font_face_load_failed<'s>( + scope: &mut v8::PinScope<'s, '_>, + face: v8::Local<'s, v8::Object>, +) -> bool { + font_face_status(scope, face).as_deref() == Some("error") +} + +pub(crate) fn load_font_faces_for_family<'s>( + scope: &mut v8::PinScope<'s, '_>, + font_set: v8::Local<'s, v8::Object>, + family: &str, +) { + let Some(faces) = super::storage::font_face_set_faces_array(scope, font_set) else { + return; + }; + for index in 0..faces.length() { + let Some(face) = faces + .get_index(scope, index) + .and_then(|face| v8::Local::::try_from(face).ok()) + else { + continue; + }; + if !font_face_string_slot(scope, face, FONT_FACE_FAMILY_SLOT) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(family)) + { + continue; + } + let _ = load_font_face(scope, face); + } +} + +fn font_face_css_source_has_url(source: &str) -> bool { + moli_css_parse::normalize_font_face_src(source) + .and_then(|source| crate::css_style::top_level_comma_separated_component_values(&source)) + .is_some_and(|sources| { + sources + .iter() + .any(|source| source.trim_start().starts_with("url(")) + }) +} + +fn font_face_status<'s>( + scope: &mut v8::PinScope<'s, '_>, + face: v8::Local<'s, v8::Object>, +) -> Option { + font_face_string_slot(scope, face, FONT_FACE_STATUS_SLOT) +} + +fn font_face_string_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + face: v8::Local<'s, v8::Object>, + slot: &str, +) -> Option { + font_face_slot_value(scope, face, slot) + .and_then(|value| v8::Local::::try_from(value).ok()) + .map(|value| value.to_rust_string_lossy(scope)) +} + +fn set_font_face_status<'s>( + scope: &mut v8::PinScope<'s, '_>, + face: v8::Local<'s, v8::Object>, + status: &'static str, +) { + let status = v8_string(scope, status).unwrap_or_else(|| v8::String::empty(scope)); + set_font_face_slot_value(scope, face, FONT_FACE_STATUS_SLOT, status.into()); +} + +fn font_face_loaded_resolver<'s>( + scope: &mut v8::PinScope<'s, '_>, + face: v8::Local<'s, v8::Object>, +) -> Option> { + font_face_slot_value(scope, face, FONT_FACE_LOADED_RESOLVER_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + .map(|object| unsafe { v8::Local::::cast_unchecked(object) }) +} + fn font_face_slot_value<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set.rs index 51232433cc..3f44424365 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set.rs @@ -1,12 +1,11 @@ -use super::events::dispatch_font_face_set_event; +use super::font_face::{font_face_load_failed, load_font_face}; use super::query::{ font_face_set_matching_faces_array, font_load_query_contains_css_wide_keyword, make_rejected_dom_exception_promise, }; use super::storage::{ array_contains_value, font_face_set_faces_array, font_face_set_manual_faces_array, - is_font_face_value, rebuild_font_face_set_faces, replace_font_face_set_ready_promise, - set_font_face_set_slot_value, set_font_face_set_status, + is_font_face_value, rebuild_font_face_set_faces, set_font_face_set_slot_value, }; use super::*; diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set/loading.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set/loading.rs index 3165e93635..aa6192164d 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set/loading.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face_set/loading.rs @@ -56,11 +56,28 @@ pub(in crate::context_bootstrap) fn font_face_set_load_callback<'s>( } let matching_faces = font_face_set_matching_faces_array(scope, this, &parsed.font) .unwrap_or_else(|| v8::Array::new(scope, 0)); - set_font_face_set_status(scope, this, "loading"); - let _ = dispatch_font_face_set_event(scope, this, "loading", None); - replace_font_face_set_ready_promise(scope, this); - set_font_face_set_status(scope, this, "loaded"); - let _ = dispatch_font_face_set_event(scope, this, "loadingdone", Some(matching_faces)); + let mut failed = false; + for index in 0..matching_faces.length() { + let Some(face) = matching_faces + .get_index(scope, index) + .and_then(|face| v8::Local::::try_from(face).ok()) + else { + continue; + }; + let _ = load_font_face(scope, face); + failed |= font_face_load_failed(scope, face); + } + if failed { + rv.set( + make_rejected_dom_exception_promise( + scope, + "NetworkError", + "One or more matching FontFace objects failed to load.", + ) + .into(), + ); + return; + } let faces_value = matching_faces.into(); match resolved_promise(scope, faces_value) { Some(promise) => rv.set(v8::Local::::from(promise)), diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands.rs b/moli-renderer-v8/src/document_runtime/mutation_commands.rs index ba00b17b89..ea23a4b010 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands.rs @@ -1844,6 +1844,7 @@ pub(crate) struct RuntimeMutationApplyResult { stylesheet_owner_changes: Vec, inline_style_attribute_csp_mutations: Vec, connected_style_csp_roots: Vec, + font_face_use_roots: Vec, } #[derive(Clone, Debug)] @@ -1880,6 +1881,7 @@ pub(super) fn finish_runtime_mutation_effects( stylesheet_owner_changes, inline_style_attribute_csp_mutations, connected_style_csp_roots, + font_face_use_roots, } = result; runtime.queue_devtools_dom_mutations(devtools_dom_mutations); @@ -1976,6 +1978,11 @@ pub(super) fn finish_runtime_mutation_effects( unsafe { &*host_ptr }, &stylesheet_owner_changes, ); + crate::native_bridge::document::load_font_faces_used_by_subtrees( + scope, + unsafe { &*host_ptr }, + &font_face_use_roots, + ); let completed_clients = prime_result.take_completed_stylesheet_clients(); runtime.settle_stylesheet_link_clients_in_current_scope(scope, host_ptr, completed_clients); runtime @@ -2215,6 +2222,15 @@ pub(super) fn prepare_runtime_mutation_effects( }; let devtools_dom_mutations = super::devtools_mutations::capture_devtools_dom_mutation_facts(dom_host, effects); + let mut font_face_use_roots = effects.tree().connected_roots().to_vec(); + for mutation in effects.style().attribute_mutations() { + if mutation.namespace().is_none() + && mutation.local_name().eq_ignore_ascii_case("style") + && !font_face_use_roots.contains(&mutation.target()) + { + font_face_use_roots.push(mutation.target()); + } + } RuntimeMutationApplyResult { changed: effects.did_change(), meta_refresh_candidates, @@ -2225,6 +2241,7 @@ pub(super) fn prepare_runtime_mutation_effects( stylesheet_owner_changes, inline_style_attribute_csp_mutations, connected_style_csp_roots, + font_face_use_roots, } } diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index d418fb9819..676288a516 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -137,7 +137,8 @@ pub(in crate::native_bridge) use css_state::{ }; pub(crate) use css_state::{ apply_stylesheet_owner_css_projections, apply_stylesheet_source_css_projection, - clear_adopted_stylesheet_font_face_wrappers, sync_document_fonts_for_handle, + clear_adopted_stylesheet_font_face_wrappers, load_font_faces_used_by_subtrees, + sync_document_fonts_for_handle, }; use css_state::{detached_document_fonts_getter, document_fonts_getter_function}; pub(crate) use css_state::{ diff --git a/moli-renderer-v8/src/native_bridge/document/css_state.rs b/moli-renderer-v8/src/native_bridge/document/css_state.rs index c33a41142c..5ed7364ef2 100644 --- a/moli-renderer-v8/src/native_bridge/document/css_state.rs +++ b/moli-renderer-v8/src/native_bridge/document/css_state.rs @@ -14,6 +14,7 @@ pub(crate) use accessors::{ node_document_adopted_style_sheets_getter_function, node_document_adopted_style_sheets_setter_function, node_document_style_sheets_getter_function, }; +pub(crate) use font_faces::load_font_faces_used_by_subtrees; pub(crate) use projection::{ apply_stylesheet_owner_css_projections, apply_stylesheet_source_css_projection, }; diff --git a/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs b/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs index b6888f19d8..380c5f3aef 100644 --- a/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs +++ b/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs @@ -15,6 +15,8 @@ use crate::{ }, util::{get_private_value, serialize_v8_iter_array, set_private_value, v8_string, v8str}, }; +use moli_v8_util::object_own_static_property_as_object; +use std::collections::HashSet; use std::convert::TryFrom; use std::rc::Rc; @@ -164,6 +166,67 @@ pub(super) fn finish_font_face_owner_projections<'s>( rebuild_connected_font_faces(scope, fonts, host, document); } +pub(crate) fn load_font_faces_used_by_subtrees( + scope: &mut v8::PinScope<'_, '_>, + host: &JsContextHost, + roots: &[DomHandle], +) { + let mut visited = HashSet::new(); + for root in roots.iter().copied() { + if !host.dom_host().is_connected(root) { + continue; + } + let Some(document) = host.dom_host().owner_document_handle(root) else { + continue; + }; + let Some(holder) = crate::util::node_wrapper_from_handle(scope, document) else { + continue; + }; + let Some(fonts) = object_own_static_property_as_object(scope, holder, FONTS_SLOT) else { + continue; + }; + for family in inline_font_families_in_subtree(host, root, &mut visited) { + crate::context_bootstrap::load_font_faces_for_family(scope, fonts, &family); + } + } +} + +fn inline_font_families_in_subtree( + host: &JsContextHost, + root: DomHandle, + visited: &mut HashSet, +) -> Vec { + let mut families = Vec::new(); + let mut stack = vec![root]; + while let Some(handle) = stack.pop() { + if !visited.insert(handle) { + continue; + } + if let Some(style) = host.dom_host().get_attribute(handle, "style") + && let Some(value) = + moli_css_parse::parse_declaration_block(&style).property_value("font-family") + && let Some(components) = + crate::css_style::top_level_comma_separated_component_values(&value) + { + for family in components { + let family = moli_css_parse::unquote_css_string(family.trim()); + if !family.is_empty() + && !families + .iter() + .any(|existing: &String| existing.eq_ignore_ascii_case(&family)) + { + families.push(family); + } + } + } + if let Some(shadow_root) = host.dom_host().shadow_root_handle(handle) { + stack.push(shadow_root); + } + stack.extend(host.dom_host().child_handles(handle)); + } + families +} + fn set_owner_font_face_contribution<'s>( scope: &mut v8::PinScope<'s, '_>, fonts: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 21de0ec80f..9c50ec0e91 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -1708,11 +1708,83 @@ fn font_face_declared_slots_ignore_prototype_spoofing() { assert_eq!( result, - r#"{"values":"Changed|url(demo.woff)|italic|700|condensed|small-caps|\"kern\"|swap|loaded|function","fake":"TypeError|TypeError|TypeError|TypeError|Promise","descriptors":["family:function:get family:0:function:set family:1:true:true:false","style:function:get style:0:function:set style:1:true:true:false","weight:function:get weight:0:function:set weight:1:true:true:false","stretch:function:get stretch:0:function:set stretch:1:true:true:false","variant:function:get variant:0:function:set variant:1:true:true:false","featureSettings:function:get featureSettings:0:function:set featureSettings:1:true:true:false","display:function:get display:0:function:set display:1:true:true:false","source:function:get source:0:undefined:undefined:undefined:true:true:false","status:function:get status:0:undefined:undefined:undefined:true:true:false","loaded:function:get loaded:0:undefined:undefined:undefined:true:true:false"],"ownSlots":[]}"# + r#"{"values":"Changed|url(demo.woff)|italic|700|condensed|small-caps|\"kern\"|swap|unloaded|function","fake":"TypeError|TypeError|TypeError|TypeError|Promise","descriptors":["family:function:get family:0:function:set family:1:true:true:false","style:function:get style:0:function:set style:1:true:true:false","weight:function:get weight:0:function:set weight:1:true:true:false","stretch:function:get stretch:0:function:set stretch:1:true:true:false","variant:function:get variant:0:function:set variant:1:true:true:false","featureSettings:function:get featureSettings:0:function:set featureSettings:1:true:true:false","display:function:get display:0:function:set display:1:true:true:false","source:function:get source:0:undefined:undefined:undefined:true:true:false","status:function:get status:0:undefined:undefined:undefined:true:true:false","loaded:function:get loaded:0:undefined:undefined:undefined:true:true:false"],"ownSlots":[]}"# ); assert_eq!(vm.eval("fakeLoadedResult").unwrap(), "rejected:TypeError"); } +#[test] +fn font_face_string_sources_follow_load_state_and_connected_style_use() { + let mut vm = new_storage_test_vm("https://font-face-string-source-state.test/"); + + vm.eval( + r#" +(() => { + const remote = new FontFace('RemoteFace', 'url(remote.woff2)'); + const remoteLoaded = remote.loaded; + const invalid = new FontFace('InvalidFace', 'not a font source'); + const local = new FontFace( + 'MissingLocalFace', + 'local("definitely-missing-moli-font")' + ); + document.fonts.add(local); + + globalThis.__fontFaceStringSourceProbe = { + remote: { + before: remote.status, + stablePromise: remote.loaded === remoteLoaded, + loadReturnsLoaded: remote.load() === remoteLoaded, + after: remote.status, + settlement: 'pending' + }, + invalid: { + status: invalid.status, + settlement: 'pending' + }, + local: { + beforeStyle: local.status, + afterDetachedStyle: '', + afterConnection: '', + settlement: 'pending' + } + }; + remoteLoaded.then( + value => { + globalThis.__fontFaceStringSourceProbe.remote.settlement = + value === remote ? 'resolved-self' : 'resolved-other'; + }, + error => { + globalThis.__fontFaceStringSourceProbe.remote.settlement = error.name; + } + ); + invalid.loaded.then( + () => { globalThis.__fontFaceStringSourceProbe.invalid.settlement = 'resolved'; }, + error => { globalThis.__fontFaceStringSourceProbe.invalid.settlement = error.name; } + ); + local.loaded.then( + () => { globalThis.__fontFaceStringSourceProbe.local.settlement = 'resolved'; }, + error => { globalThis.__fontFaceStringSourceProbe.local.settlement = error.name; } + ); + + const target = document.createElement('div'); + target.style.fontFamily = 'MissingLocalFace'; + globalThis.__fontFaceStringSourceProbe.local.afterDetachedStyle = local.status; + (document.body || document.documentElement || document).appendChild(target); + globalThis.__fontFaceStringSourceProbe.local.afterConnection = local.status; +})() +"#, + ) + .expect("string-backed FontFace state probe should initialize"); + + let result = vm + .eval("JSON.stringify(globalThis.__fontFaceStringSourceProbe)") + .expect("string-backed FontFace promises should settle"); + assert_eq!( + result, + r#"{"remote":{"before":"unloaded","stablePromise":true,"loadReturnsLoaded":true,"after":"loaded","settlement":"resolved-self"},"invalid":{"status":"error","settlement":"SyntaxError"},"local":{"beforeStyle":"unloaded","afterDetachedStyle":"unloaded","afterConnection":"error","settlement":"NetworkError"}}"# + ); +} + #[test] fn font_face_variation_settings_use_stylo_descriptor_serialization() { let mut vm = new_storage_test_vm("https://font-face-variation-settings.test/"); diff --git a/moli-wpt-compat/fixtures/wpt/ported/css-font-loading/fontface-basic.html b/moli-wpt-compat/fixtures/wpt/ported/css-font-loading/fontface-basic.html index 9f9b732111..7786a666cb 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/css-font-loading/fontface-basic.html +++ b/moli-wpt-compat/fixtures/wpt/ported/css-font-loading/fontface-basic.html @@ -34,7 +34,7 @@ test(function () { assert_equals(face.variant, "small-caps"); assert_equals(face.featureSettings, '"liga" 0'); assert_equals(face.display, "swap"); - assert_equals(face.status, "loaded"); + assert_equals(face.status, "unloaded"); assert_true(face.loaded instanceof Promise); assert_true(face.load() instanceof Promise); }, "FontFace exposes constructor metadata and descriptors"); @@ -69,9 +69,10 @@ test(function () { promise_test(async function () { const face = new FontFace("PromiseFace", "url(promise.woff)"); + const loaded = face.loaded; - assert_equals(await face.loaded, face); assert_equals(await face.load(), face); + assert_equals(await loaded, face); }, "FontFace loaded and load promises resolve with the face"); promise_test(async function () { diff --git a/moli-wpt-compat/fixtures/wpt/ported/idl/fontface-idlharness.html b/moli-wpt-compat/fixtures/wpt/ported/idl/fontface-idlharness.html index ec40b96259..3416510b25 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/idl/fontface-idlharness.html +++ b/moli-wpt-compat/fixtures/wpt/ported/idl/fontface-idlharness.html @@ -87,7 +87,7 @@ promise_test(async function () { assert_equals(fonts.ready, fonts.ready); assert_equals(face.family, "FixtureFace"); assert_equals(face.source, "url(fixture.woff2)"); - assert_equals(face.status, "loaded"); + assert_equals(face.status, "unloaded"); assert_equals(await face.load(), face); assert_equals(await face.loaded, face);