script/dom/
fontface.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7
8use cssparser::{Parser, ParserInput};
9use dom_struct::dom_struct;
10use fonts::{FontContext, FontContextWebFontMethods, FontTemplate, LowercaseFontFamilyName};
11use js::rust::HandleObject;
12use style::error_reporting::ParseErrorReporter;
13use style::font_face::SourceList;
14use style::parser::ParserContext;
15use style::stylesheets::{CssRuleType, FontFaceRule, Origin, UrlExtraData};
16use style_traits::{ParsingMode, ToCss};
17
18use super::bindings::cell::DomRefCell;
19use super::bindings::codegen::UnionTypes::StringOrArrayBufferViewOrArrayBuffer;
20use super::bindings::error::{Error, ErrorResult, Fallible};
21use super::bindings::refcounted::Trusted;
22use super::bindings::reflector::DomGlobal;
23use super::bindings::root::MutNullableDom;
24use super::types::FontFaceSet;
25use crate::dom::bindings::codegen::Bindings::FontFaceBinding::{
26    FontFaceDescriptors, FontFaceLoadStatus, FontFaceMethods,
27};
28use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
29use crate::dom::bindings::codegen::UnionTypes;
30use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
31use crate::dom::bindings::root::DomRoot;
32use crate::dom::bindings::str::DOMString;
33use crate::dom::globalscope::GlobalScope;
34use crate::dom::promise::Promise;
35use crate::dom::window::Window;
36use crate::script_runtime::CanGc;
37
38/// <https://drafts.csswg.org/css-font-loading/#fontface-interface>
39#[dom_struct]
40pub struct FontFace {
41    reflector: Reflector,
42    status: Cell<FontFaceLoadStatus>,
43    family_name: DomRefCell<DOMString>,
44    descriptors: DomRefCell<FontFaceDescriptors>,
45
46    /// A reference to the [`FontFaceSet`] that this `FontFace` is a member of, if it has been
47    /// added to one. `None` otherwise. The spec suggests that a `FontFace` can be a member of
48    /// multiple `FontFaceSet`s, but this doesn't seem to be the case in practice, as the
49    /// `FontFaceSet` constructor is not exposed on the global scope.
50    font_face_set: MutNullableDom<FontFaceSet>,
51
52    /// This holds the [`FontTemplate`] resulting from loading this `FontFace`, to be used when the
53    /// `FontFace` is added to the global `FontFaceSet` and thus the `[FontContext]`.
54    //
55    // TODO: This could potentially share the `FontTemplateRef` created by `FontContext`, rather
56    // than having its own copy of the template.
57    #[no_trace = "Does not contain managed objects"]
58    template: RefCell<Option<(LowercaseFontFamilyName, FontTemplate)>>,
59
60    #[no_trace = "Does not contain managed objects"]
61    /// <https://drafts.csswg.org/css-font-loading/#m-fontface-urls-slot>
62    urls: DomRefCell<Option<SourceList>>,
63
64    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-fontstatuspromise-slot>
65    #[ignore_malloc_size_of = "Rc"]
66    font_status_promise: Rc<Promise>,
67}
68
69/// Given the various font face descriptors, construct the equivalent `@font-face` css rule as a
70/// string and parse it using `style` crate. Returns `Err(Error::Syntax)` if parsing fails.
71///
72/// Due to lack of support in the `style` crate, parsing the whole `@font-face` rule is much easier
73/// to implement than parsing each declaration on its own.
74fn parse_font_face_descriptors(
75    global: &GlobalScope,
76    family_name: &DOMString,
77    sources: Option<&str>,
78    input_descriptors: &FontFaceDescriptors,
79) -> Fallible<FontFaceRule> {
80    let window = global.as_window(); // TODO: Support calling FontFace APIs from Worker
81    let quirks_mode = window.Document().quirks_mode();
82    let url_data = UrlExtraData(window.get_url().get_arc());
83    let error_reporter = FontFaceErrorReporter {
84        not_encountered_error: Cell::new(true),
85    };
86    let parser_context = ParserContext::new(
87        Origin::Author,
88        &url_data,
89        Some(CssRuleType::FontFace),
90        ParsingMode::DEFAULT,
91        quirks_mode,
92        /* namespaces = */ Default::default(),
93        Some(&error_reporter as &dyn ParseErrorReporter),
94        None,
95    );
96
97    let FontFaceDescriptors {
98        ascentOverride,
99        descentOverride,
100        display,
101        featureSettings,
102        lineGapOverride,
103        stretch,
104        style,
105        unicodeRange,
106        variationSettings,
107        weight,
108    } = input_descriptors;
109
110    let _ = variationSettings; // TODO: Stylo doesn't parse font-variation-settings yet.
111    let maybe_sources = sources.map_or_else(String::new, |sources| format!("src: {sources};"));
112    let font_face_rule = format!(
113        r"
114        ascent-override: {ascentOverride};
115        descent-override: {descentOverride};
116        font-display: {display};
117        font-family: {family_name};
118        font-feature-settings: {featureSettings};
119        font-stretch: {stretch};
120        font-style: {style};
121        font-weight: {weight};
122        line-gap-override: {lineGapOverride};
123        unicode-range: {unicodeRange};
124        {maybe_sources}
125    "
126    );
127
128    // TODO: Should this be the source location in the script that invoked the font face API?
129    let location = cssparser::SourceLocation { line: 0, column: 0 };
130    let mut input = ParserInput::new(&font_face_rule);
131    let mut parser = Parser::new(&mut input);
132    let mut parsed_font_face_rule =
133        style::font_face::parse_font_face_block(&parser_context, &mut parser, location);
134
135    if let Some(ref mut sources) = parsed_font_face_rule.sources {
136        let supported_sources: Vec<_> = sources
137            .0
138            .iter()
139            .rev()
140            .filter(FontContext::is_supported_web_font_source)
141            .cloned()
142            .collect();
143        if supported_sources.is_empty() {
144            error_reporter.not_encountered_error.set(false);
145        } else {
146            sources.0 = supported_sources;
147        }
148    }
149
150    if error_reporter.not_encountered_error.get() {
151        Ok(parsed_font_face_rule)
152    } else {
153        Err(Error::Syntax)
154    }
155}
156
157fn serialize_parsed_descriptors(font_face_rule: &FontFaceRule) -> FontFaceDescriptors {
158    FontFaceDescriptors {
159        ascentOverride: font_face_rule.ascent_override.to_css_string().into(),
160        descentOverride: font_face_rule.descent_override.to_css_string().into(),
161        display: font_face_rule.display.to_css_string().into(),
162        featureSettings: font_face_rule.feature_settings.to_css_string().into(),
163        lineGapOverride: font_face_rule.line_gap_override.to_css_string().into(),
164        stretch: font_face_rule.stretch.to_css_string().into(),
165        style: font_face_rule.style.to_css_string().into(),
166        unicodeRange: font_face_rule.unicode_range.to_css_string().into(),
167        variationSettings: font_face_rule.variation_settings.to_css_string().into(),
168        weight: font_face_rule.weight.to_css_string().into(),
169    }
170}
171
172struct FontFaceErrorReporter {
173    not_encountered_error: Cell<bool>,
174}
175
176impl ParseErrorReporter for FontFaceErrorReporter {
177    fn report_error(
178        &self,
179        _url: &UrlExtraData,
180        _location: cssparser::SourceLocation,
181        _error: style::error_reporting::ContextualParseError,
182    ) {
183        self.not_encountered_error.set(false);
184    }
185}
186
187impl FontFace {
188    /// Construct a [`FontFace`] to be used in the case of failure in parsing the
189    /// font face descriptors.
190    fn new_failed_font_face(global: &GlobalScope, can_gc: CanGc) -> Self {
191        let font_status_promise = Promise::new(global, can_gc);
192        // If any of them fail to parse correctly, reject font face’s [[FontStatusPromise]] with a
193        // DOMException named "SyntaxError"
194        font_status_promise.reject_error(Error::Syntax, can_gc);
195
196        // set font face’s corresponding attributes to the empty string, and set font face’s status
197        // attribute to "error"
198        Self {
199            reflector: Reflector::new(),
200            font_face_set: MutNullableDom::default(),
201            font_status_promise,
202            family_name: DomRefCell::default(),
203            urls: DomRefCell::default(),
204            descriptors: DomRefCell::new(FontFaceDescriptors {
205                ascentOverride: DOMString::new(),
206                descentOverride: DOMString::new(),
207                display: DOMString::new(),
208                featureSettings: DOMString::new(),
209                lineGapOverride: DOMString::new(),
210                stretch: DOMString::new(),
211                style: DOMString::new(),
212                unicodeRange: DOMString::new(),
213                variationSettings: DOMString::new(),
214                weight: DOMString::new(),
215            }),
216            status: Cell::new(FontFaceLoadStatus::Error),
217            template: RefCell::default(),
218        }
219    }
220
221    /// <https://drafts.csswg.org/css-font-loading/#font-face-constructor>
222    fn new_inherited(
223        global: &GlobalScope,
224        family_name: DOMString,
225        source: StringOrArrayBufferViewOrArrayBuffer,
226        descriptors: &FontFaceDescriptors,
227        can_gc: CanGc,
228    ) -> Self {
229        // TODO: Add support for ArrayBuffer and ArrayBufferView sources.
230        let StringOrArrayBufferViewOrArrayBuffer::String(ref source_string) = source else {
231            return Self::new_failed_font_face(global, can_gc);
232        };
233
234        // Step 1. Parse the family argument, and the members of the descriptors argument,
235        // according to the grammars of the corresponding descriptors of the CSS @font-face rule If
236        // the source argument is a CSSOMString, parse it according to the grammar of the CSS src
237        // descriptor of the @font-face rule.
238        let parse_result =
239            parse_font_face_descriptors(global, &family_name, Some(source_string), descriptors);
240
241        let Ok(ref parsed_font_face_rule) = parse_result else {
242            // If any of them fail to parse correctly, reject font face’s
243            // [[FontStatusPromise]] with a DOMException named "SyntaxError", set font face’s
244            // corresponding attributes to the empty string, and set font face’s status attribute
245            // to "error".
246            return Self::new_failed_font_face(global, can_gc);
247        };
248
249        // Set its internal [[FontStatusPromise]] slot to a fresh pending Promise object.
250        let font_status_promise = Promise::new(global, can_gc);
251
252        let sources = parsed_font_face_rule
253            .sources
254            .clone()
255            .expect("Sources should be non-None after validation");
256
257        // Let font face be a fresh FontFace object.
258        Self {
259            reflector: Reflector::new(),
260
261            // Set font face’s status attribute to "unloaded".
262            status: Cell::new(FontFaceLoadStatus::Unloaded),
263
264            // Set font face’s corresponding attributes to the serialization of the parsed values.
265            descriptors: DomRefCell::new(serialize_parsed_descriptors(parsed_font_face_rule)),
266
267            font_face_set: MutNullableDom::default(),
268            family_name: DomRefCell::new(family_name.clone()),
269            urls: DomRefCell::new(Some(sources)),
270            template: RefCell::default(),
271            font_status_promise,
272        }
273    }
274
275    pub(crate) fn new(
276        global: &GlobalScope,
277        proto: Option<HandleObject>,
278        font_family: DOMString,
279        source: StringOrArrayBufferViewOrArrayBuffer,
280        descriptors: &FontFaceDescriptors,
281        can_gc: CanGc,
282    ) -> DomRoot<Self> {
283        reflect_dom_object_with_proto(
284            Box::new(Self::new_inherited(
285                global,
286                font_family,
287                source,
288                descriptors,
289                can_gc,
290            )),
291            global,
292            proto,
293            can_gc,
294        )
295    }
296
297    pub(super) fn set_associated_font_face_set(&self, font_face_set: &FontFaceSet) {
298        self.font_face_set.set(Some(font_face_set));
299    }
300
301    pub(super) fn loaded(&self) -> bool {
302        self.status.get() == FontFaceLoadStatus::Loaded
303    }
304
305    pub(super) fn template(&self) -> Option<(LowercaseFontFamilyName, FontTemplate)> {
306        self.template.borrow().clone()
307    }
308
309    /// Implements the body of the setter for the descriptor attributes of the [`FontFace`] interface.
310    ///
311    /// <https://drafts.csswg.org/css-font-loading/#fontface-interface>:
312    /// On setting, parse the string according to the grammar for the corresponding @font-face
313    /// descriptor. If it does not match the grammar, throw a SyntaxError; otherwise, set the attribute
314    /// to the serialization of the parsed value.
315    fn validate_and_set_descriptors(&self, new_descriptors: FontFaceDescriptors) -> ErrorResult {
316        let global = self.global();
317        let parsed_font_face_rule = parse_font_face_descriptors(
318            &global,
319            &self.family_name.borrow(),
320            None,
321            &new_descriptors,
322        )?;
323
324        *self.descriptors.borrow_mut() = serialize_parsed_descriptors(&parsed_font_face_rule);
325        Ok(())
326    }
327}
328
329impl FontFaceMethods<crate::DomTypeHolder> for FontFace {
330    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-family>
331    fn Family(&self) -> DOMString {
332        self.family_name.borrow().clone()
333    }
334
335    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-family>
336    fn SetFamily(&self, family_name: DOMString) -> ErrorResult {
337        let descriptors = self.descriptors.borrow();
338        let global = self.global();
339        let _ = parse_font_face_descriptors(&global, &family_name, None, &descriptors)?;
340        *self.family_name.borrow_mut() = family_name;
341        Ok(())
342    }
343
344    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-style>
345    fn Style(&self) -> DOMString {
346        self.descriptors.borrow().style.clone()
347    }
348
349    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-style>
350    fn SetStyle(&self, value: DOMString) -> ErrorResult {
351        let mut new_descriptors = self.descriptors.borrow().clone();
352        new_descriptors.style = value;
353        self.validate_and_set_descriptors(new_descriptors)
354    }
355
356    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-weight>
357    fn Weight(&self) -> DOMString {
358        self.descriptors.borrow().weight.clone()
359    }
360
361    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-weight>
362    fn SetWeight(&self, value: DOMString) -> ErrorResult {
363        let mut new_descriptors = self.descriptors.borrow().clone();
364        new_descriptors.weight = value;
365        self.validate_and_set_descriptors(new_descriptors)
366    }
367
368    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-stretch>
369    fn Stretch(&self) -> DOMString {
370        self.descriptors.borrow().stretch.clone()
371    }
372
373    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-stretch>
374    fn SetStretch(&self, value: DOMString) -> ErrorResult {
375        let mut new_descriptors = self.descriptors.borrow().clone();
376        new_descriptors.stretch = value;
377        self.validate_and_set_descriptors(new_descriptors)
378    }
379
380    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-unicoderange>
381    fn UnicodeRange(&self) -> DOMString {
382        self.descriptors.borrow().unicodeRange.clone()
383    }
384
385    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-unicoderange>
386    fn SetUnicodeRange(&self, value: DOMString) -> ErrorResult {
387        let mut new_descriptors = self.descriptors.borrow().clone();
388        new_descriptors.unicodeRange = value;
389        self.validate_and_set_descriptors(new_descriptors)
390    }
391
392    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-featuresettings>
393    fn FeatureSettings(&self) -> DOMString {
394        self.descriptors.borrow().featureSettings.clone()
395    }
396
397    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-featuresettings>
398    fn SetFeatureSettings(&self, value: DOMString) -> ErrorResult {
399        let mut new_descriptors = self.descriptors.borrow().clone();
400        new_descriptors.featureSettings = value;
401        self.validate_and_set_descriptors(new_descriptors)
402    }
403
404    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-variationsettings>
405    fn VariationSettings(&self) -> DOMString {
406        self.descriptors.borrow().variationSettings.clone()
407    }
408
409    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-variationsettings>
410    fn SetVariationSettings(&self, value: DOMString) -> ErrorResult {
411        let mut new_descriptors = self.descriptors.borrow().clone();
412        new_descriptors.variationSettings = value;
413        self.validate_and_set_descriptors(new_descriptors)
414    }
415
416    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-display>
417    fn Display(&self) -> DOMString {
418        self.descriptors.borrow().display.clone()
419    }
420
421    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-display>
422    fn SetDisplay(&self, value: DOMString) -> ErrorResult {
423        let mut new_descriptors = self.descriptors.borrow().clone();
424        new_descriptors.display = value;
425        self.validate_and_set_descriptors(new_descriptors)
426    }
427
428    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-ascentoverride>
429    fn AscentOverride(&self) -> DOMString {
430        self.descriptors.borrow().ascentOverride.clone()
431    }
432
433    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-ascentoverride>
434    fn SetAscentOverride(&self, value: DOMString) -> ErrorResult {
435        let mut new_descriptors = self.descriptors.borrow().clone();
436        new_descriptors.ascentOverride = value;
437        self.validate_and_set_descriptors(new_descriptors)
438    }
439
440    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-descentoverride>
441    fn DescentOverride(&self) -> DOMString {
442        self.descriptors.borrow().descentOverride.clone()
443    }
444
445    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-descentoverride>
446    fn SetDescentOverride(&self, value: DOMString) -> ErrorResult {
447        let mut new_descriptors = self.descriptors.borrow().clone();
448        new_descriptors.descentOverride = value;
449        self.validate_and_set_descriptors(new_descriptors)
450    }
451
452    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-linegapoverride>
453    fn LineGapOverride(&self) -> DOMString {
454        self.descriptors.borrow().lineGapOverride.clone()
455    }
456
457    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-linegapoverride>
458    fn SetLineGapOverride(&self, value: DOMString) -> ErrorResult {
459        let mut new_descriptors = self.descriptors.borrow().clone();
460        new_descriptors.lineGapOverride = value;
461        self.validate_and_set_descriptors(new_descriptors)
462    }
463
464    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-status>
465    fn Status(&self) -> FontFaceLoadStatus {
466        self.status.get()
467    }
468
469    /// The load() method of FontFace forces a url-based font face to request its font data and
470    /// load. For fonts constructed from a buffer source, or fonts that are already loading or
471    /// loaded, it does nothing.
472    /// <https://drafts.csswg.org/css-font-loading/#font-face-load>
473    fn Load(&self) -> Rc<Promise> {
474        let Some(sources) = self.urls.borrow_mut().take() else {
475            // Step 2. If font face’s [[Urls]] slot is null, or its status attribute is anything
476            // other than "unloaded", return font face’s [[FontStatusPromise]] and abort these
477            // steps.
478            return self.font_status_promise.clone();
479        };
480
481        // FontFace must not be loaded at this point as `self.urls` is not None, implying `Load`
482        // wasn't called already. In our implementation, `urls` is set after parsing, so it
483        // cannot be `Some` if the status is `Error`.
484        debug_assert_eq!(self.status.get(), FontFaceLoadStatus::Unloaded);
485
486        let global = self.global();
487        let trusted = Trusted::new(self);
488        let task_source = global
489            .task_manager()
490            .font_loading_task_source()
491            .to_sendable();
492
493        let finished_callback = Box::new(
494            move |family_name: LowercaseFontFamilyName, load_result: Option<_>| {
495                let trusted = trusted.clone();
496
497                // Step 5. When the load operation completes, successfully or not, queue a task to
498                // run the following steps synchronously:
499                task_source.queue(task!(resolve_font_face_load_task: move || {
500                    let font_face = trusted.root();
501
502                    match load_result {
503                        None => {
504                            // Step 5.1. If the attempt to load fails, reject font face’s
505                            // [[FontStatusPromise]] with a DOMException whose name is "NetworkError"
506                            // and set font face’s status attribute to "error".
507                            font_face.status.set(FontFaceLoadStatus::Error);
508                            font_face.font_status_promise.reject_error(Error::Network, CanGc::note());
509                        }
510                        Some(template) => {
511                            // Step 5.2. Otherwise, font face now represents the loaded font;
512                            // fulfill font face’s [[FontStatusPromise]] with font face and set
513                            // font face’s status attribute to "loaded".
514                            font_face.status.set(FontFaceLoadStatus::Loaded);
515                            let old_template = font_face.template.borrow_mut().replace((family_name, template));
516                            debug_assert!(old_template.is_none(), "FontFace's template must be intialized only once");
517                            font_face.font_status_promise.resolve_native(&font_face, CanGc::note());
518                        }
519                    }
520
521                    if let Some(font_face_set) = font_face.font_face_set.get() {
522                        // For each FontFaceSet font face is in: ...
523                        //
524                        // This implements steps 5.1.1, 5.1.2, 5.2.1 and 5.2.2 - these
525                        // take care of changing the status of the `FontFaceSet` in which this
526                        // `FontFace` is a member, for both failed and successful load.
527                        font_face_set.handle_font_face_status_changed(&font_face);
528                    }
529                }));
530            },
531        );
532
533        // We parse the descriptors again because they are stored as `DOMString`s in this `FontFace`
534        // but the `load_web_font_for_script` API needs parsed values.
535        let parsed_font_face_rule = parse_font_face_descriptors(
536            &global,
537            &self.family_name.borrow(),
538            None,
539            &self.descriptors.borrow(),
540        )
541        .expect("Parsing shouldn't fail as descriptors are valid by construction");
542
543        // Step 4. Using the value of font face’s [[Urls]] slot, attempt to load a font as defined
544        // in [CSS-FONTS-3], as if it was the value of a @font-face rule’s src descriptor.
545        // TODO: FontFaceSet is not supported on Workers yet. The `as_window` call below should be
546        // replaced when we do support it.
547        global.as_window().font_context().load_web_font_for_script(
548            global.webview_id(),
549            sources,
550            (&parsed_font_face_rule).into(),
551            finished_callback,
552        );
553
554        // Step 3. Set font face’s status attribute to "loading", return font face’s
555        // [[FontStatusPromise]], and continue executing the rest of this algorithm asynchronously.
556        self.status.set(FontFaceLoadStatus::Loading);
557        self.font_status_promise.clone()
558    }
559
560    /// <https://drafts.csswg.org/css-font-loading/#dom-fontface-loaded>
561    fn Loaded(&self) -> Rc<Promise> {
562        self.font_status_promise.clone()
563    }
564
565    /// <https://drafts.csswg.org/css-font-loading/#font-face-constructor>
566    fn Constructor(
567        window: &Window,
568        proto: Option<HandleObject>,
569        can_gc: CanGc,
570        family: DOMString,
571        source: UnionTypes::StringOrArrayBufferViewOrArrayBuffer,
572        descriptors: &FontFaceDescriptors,
573    ) -> DomRoot<FontFace> {
574        let global = window.as_global_scope();
575        FontFace::new(global, proto, family, source, descriptors, can_gc)
576    }
577}