mirror of
https://github.com/lexmount/moli.git
synced 2026-10-20 08:01:36 +00:00
fix(fonts): lazily create FontFace loaded promise
This commit is contained in:
@@ -21,6 +21,7 @@ pub(super) const FONT_FACE_DISPLAY_SLOT: &str = "__moliFontFaceDisplay";
|
||||
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_ERROR_SLOT: &str = "__moliFontFaceError";
|
||||
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";
|
||||
|
||||
@@ -33,6 +33,8 @@ struct FontFaceObjectDeclaration<'s> {
|
||||
loaded: Option<v8::Local<'s, v8::Promise>>,
|
||||
#[webapi(slot = FONT_FACE_LOADED_RESOLVER_SLOT)]
|
||||
loaded_resolver: Option<v8::Local<'s, v8::PromiseResolver>>,
|
||||
#[webapi(slot = FONT_FACE_ERROR_SLOT)]
|
||||
error: Option<v8::Local<'s, v8::Value>>,
|
||||
#[webapi(slot = FONT_FACE_SET_OWNERS_SLOT, constructor_default = Vec::new())]
|
||||
owner_sets: Vec<v8::Local<'s, v8::Value>>,
|
||||
#[webapi(slot = FONT_FACE_LOAD_NOTIFICATION_SENT_SLOT, constructor_default = false)]
|
||||
@@ -122,8 +124,7 @@ struct FontFacePrototypeAccessorsDeclaration {
|
||||
status: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = font_face_readonly_attribute_getter_callback,
|
||||
data = callback_data_index_value(scope, 2),
|
||||
getter = font_face_loaded_getter_callback,
|
||||
returns_promise,
|
||||
enumerable
|
||||
)]
|
||||
@@ -190,6 +191,18 @@ fn font_face_readonly_attribute_getter_callback<'s>(
|
||||
rv.set(value);
|
||||
}
|
||||
|
||||
fn font_face_loaded_getter_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(loaded) = ensure_font_face_loaded_promise(scope, args.this()) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
rv.set(loaded.into());
|
||||
}
|
||||
|
||||
fn font_face_attribute_setter_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -254,36 +267,32 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>(
|
||||
return;
|
||||
};
|
||||
let display = descriptor_string_property(scope, descriptors, "display", "auto");
|
||||
let (source, status, loaded, loaded_resolver) = match parsed.source {
|
||||
let (source, status, error) = 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))
|
||||
(source, "unloaded", None)
|
||||
}
|
||||
FontFaceConstructorSource::Css(source) => {
|
||||
let loaded = super::query::make_rejected_dom_exception_promise(
|
||||
let error = crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
"SyntaxError",
|
||||
"Invalid FontFace source descriptor.",
|
||||
"SyntaxError",
|
||||
);
|
||||
(source, "error", Some(loaded), None)
|
||||
(source, "error", Some(error))
|
||||
}
|
||||
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, None)
|
||||
(String::new(), "loaded", None)
|
||||
}
|
||||
FontFaceConstructorSource::Binary(_) => {
|
||||
let loaded = super::query::make_rejected_dom_exception_promise(
|
||||
let error = crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
"SyntaxError",
|
||||
"Invalid font data in ArrayBuffer.",
|
||||
"SyntaxError",
|
||||
);
|
||||
(String::new(), "error", Some(loaded), None)
|
||||
(String::new(), "error", Some(error))
|
||||
}
|
||||
};
|
||||
FontFaceObjectDeclaration::new(
|
||||
@@ -297,8 +306,9 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>(
|
||||
variation_settings,
|
||||
display,
|
||||
status,
|
||||
loaded,
|
||||
loaded_resolver,
|
||||
None,
|
||||
None,
|
||||
error,
|
||||
)
|
||||
.initialize(scope, this)
|
||||
.expect("FontFace declaration should initialize object");
|
||||
@@ -338,11 +348,7 @@ const FONT_FACE_WRITABLE_ATTRIBUTE_SLOTS: &[&str] = &[
|
||||
FONT_FACE_DISPLAY_SLOT,
|
||||
];
|
||||
|
||||
const FONT_FACE_READONLY_ATTRIBUTE_SLOTS: &[&str] = &[
|
||||
FONT_FACE_SOURCE_SLOT,
|
||||
FONT_FACE_STATUS_SLOT,
|
||||
FONT_FACE_LOADED_SLOT,
|
||||
];
|
||||
const FONT_FACE_READONLY_ATTRIBUTE_SLOTS: &[&str] = &[FONT_FACE_SOURCE_SLOT, FONT_FACE_STATUS_SLOT];
|
||||
|
||||
pub(in crate::context_bootstrap) fn font_face_load_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
@@ -350,8 +356,10 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>(
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let this = args.this();
|
||||
if let Some(loaded) = load_font_face(scope, this) {
|
||||
rv.set(loaded);
|
||||
let loaded = ensure_font_face_loaded_promise(scope, this);
|
||||
start_font_face_load(scope, this);
|
||||
if let Some(loaded) = loaded {
|
||||
rv.set(loaded.into());
|
||||
return;
|
||||
}
|
||||
match resolved_promise(scope, this.into()) {
|
||||
@@ -360,14 +368,13 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn load_font_face<'s>(
|
||||
pub(super) fn start_font_face_load<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
face: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
set_font_face_status(scope, face, "loading");
|
||||
@@ -379,20 +386,22 @@ pub(super) fn load_font_face<'s>(
|
||||
set_font_face_status(scope, face, "error");
|
||||
}
|
||||
|
||||
if let Some(resolver) = font_face_loaded_resolver(scope, face) {
|
||||
if succeeds {
|
||||
if succeeds {
|
||||
if let Some(resolver) = font_face_loaded_resolver(scope, face) {
|
||||
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",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let exception = crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
"No source in the FontFace src list could be loaded.",
|
||||
"NetworkError",
|
||||
);
|
||||
set_font_face_slot_value(scope, face, FONT_FACE_ERROR_SLOT, exception);
|
||||
if let Some(resolver) = font_face_loaded_resolver(scope, face) {
|
||||
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>(
|
||||
@@ -422,7 +431,7 @@ pub(crate) fn load_font_faces_for_family<'s>(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let _ = load_font_face(scope, face);
|
||||
start_font_face_load(scope, face);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +480,39 @@ fn font_face_loaded_resolver<'s>(
|
||||
.map(|object| unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(object) })
|
||||
}
|
||||
|
||||
fn ensure_font_face_loaded_promise<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
face: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Promise>> {
|
||||
if let Some(loaded) = font_face_slot_value(scope, face, FONT_FACE_LOADED_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Promise>::try_from(value).ok())
|
||||
{
|
||||
return Some(loaded);
|
||||
}
|
||||
let status = font_face_status(scope, face)?;
|
||||
let resolver = v8::PromiseResolver::new(scope)?;
|
||||
let loaded = resolver.get_promise(scope);
|
||||
set_font_face_slot_value(scope, face, FONT_FACE_LOADED_SLOT, loaded.into());
|
||||
match status.as_str() {
|
||||
"loaded" => {
|
||||
let _ = resolver.resolve(scope, face.into());
|
||||
}
|
||||
"error" => {
|
||||
let error =
|
||||
font_face_slot_value(scope, face, FONT_FACE_ERROR_SLOT).unwrap_or_else(|| {
|
||||
crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
"The FontFace failed to load.",
|
||||
"NetworkError",
|
||||
)
|
||||
});
|
||||
let _ = resolver.reject(scope, error);
|
||||
}
|
||||
_ => set_font_face_slot_value(scope, face, FONT_FACE_LOADED_RESOLVER_SLOT, resolver.into()),
|
||||
}
|
||||
Some(loaded)
|
||||
}
|
||||
|
||||
fn descriptor_string_property(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
object: Option<v8::Local<'_, v8::Object>>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::font_face::{font_face_load_failed, load_font_face};
|
||||
use super::font_face::{font_face_load_failed, start_font_face_load};
|
||||
use super::query::{
|
||||
font_face_set_matching_faces_array, font_load_query_contains_css_wide_keyword,
|
||||
make_rejected_dom_exception_promise,
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ pub(in crate::context_bootstrap) fn font_face_set_load_callback<'s>(
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let _ = load_font_face(scope, face);
|
||||
start_font_face_load(scope, face);
|
||||
failed |= font_face_load_failed(scope, face);
|
||||
}
|
||||
if failed {
|
||||
|
||||
@@ -1791,6 +1791,69 @@ fn font_face_declared_slots_ignore_prototype_spoofing() {
|
||||
assert_eq!(vm.eval("fakeLoadedResult").unwrap(), "rejected:TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_font_face_defers_loaded_rejection_until_promise_is_observed() {
|
||||
let mut vm = new_storage_test_vm("https://font-face-lazy-loaded-promise.test/");
|
||||
|
||||
let initial = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__fontFaceUnhandled = [];
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
__fontFaceUnhandled.push(event.reason && event.reason.name);
|
||||
event.preventDefault();
|
||||
});
|
||||
globalThis.__invalidLazyFontFace = new FontFace(
|
||||
'InvalidLazyFace',
|
||||
'not a font source'
|
||||
);
|
||||
return JSON.stringify({
|
||||
status: __invalidLazyFontFace.status,
|
||||
unhandled: __fontFaceUnhandled
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("invalid FontFace construction should not throw");
|
||||
assert_eq!(initial, r#"{"status":"error","unhandled":[]}"#);
|
||||
|
||||
for _ in 0..4 {
|
||||
vm.eval("JSON.stringify(__fontFaceUnhandled)")
|
||||
.expect("unobserved FontFace rejection checkpoints should drain");
|
||||
}
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(__fontFaceUnhandled)")
|
||||
.expect("unobserved FontFace rejection result should evaluate"),
|
||||
"[]",
|
||||
);
|
||||
|
||||
let observed = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const first = __invalidLazyFontFace.loaded;
|
||||
const second = __invalidLazyFontFace.loaded;
|
||||
globalThis.__invalidLazyFontFaceRejection = 'pending';
|
||||
first.catch(error => {
|
||||
__invalidLazyFontFaceRejection = error.name;
|
||||
});
|
||||
return JSON.stringify({
|
||||
stable: first === second,
|
||||
loadReturnsLoaded: __invalidLazyFontFace.load() === first
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("observed invalid FontFace loaded promise should evaluate");
|
||||
assert_eq!(observed, r#"{"stable":true,"loadReturnsLoaded":true}"#,);
|
||||
assert_eq!(
|
||||
vm.eval("__invalidLazyFontFaceRejection")
|
||||
.expect("invalid FontFace loaded rejection should settle"),
|
||||
"SyntaxError",
|
||||
);
|
||||
}
|
||||
|
||||
#[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/");
|
||||
|
||||
Reference in New Issue
Block a user