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