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