ruma_html/html/
matrix.rs

1//! Types to work with HTML elements and attributes [suggested by the Matrix Specification][spec].
2//!
3//! [spec]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
4
5use std::collections::BTreeSet;
6
7use html5ever::{ns, tendril::StrTendril, Attribute, QualName};
8use ruma_common::{
9    IdParseError, MatrixToError, MatrixToUri, MatrixUri, MatrixUriError, MxcUri, OwnedMxcUri,
10};
11
12use crate::sanitizer_config::clean::{compat, spec};
13
14const CLASS_LANGUAGE_PREFIX: &str = "language-";
15
16/// The data of a Matrix HTML element.
17///
18/// This is a helper type to work with elements [suggested by the Matrix Specification][spec].
19///
20/// This performs a lossless conversion from [`ElementData`]. Unsupported elements are represented
21/// by [`MatrixElement::Other`] and unsupported attributes are listed in the `attrs` field.
22///
23/// [`ElementData`]: crate::ElementData
24/// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
25#[derive(Debug, Clone)]
26#[allow(clippy::exhaustive_structs)]
27pub struct MatrixElementData {
28    /// The HTML element and its supported data.
29    pub element: MatrixElement,
30
31    /// The unsupported attributes found on the element.
32    pub attrs: BTreeSet<Attribute>,
33}
34
35impl MatrixElementData {
36    /// Parse a `MatrixElementData` from the given qualified name and attributes.
37    #[allow(clippy::mutable_key_type)]
38    pub(super) fn parse(name: &QualName, attrs: &BTreeSet<Attribute>) -> Self {
39        let (element, attrs) = MatrixElement::parse(name, attrs);
40        Self { element, attrs }
41    }
42}
43
44/// A Matrix HTML element.
45///
46/// All the elements [suggested by the Matrix Specification][spec] have a variant. The others are
47/// handled by the fallback `Other` variant.
48///
49/// Suggested attributes are represented as optional fields on the variants structs.
50///
51/// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
52#[derive(Debug, Clone)]
53#[non_exhaustive]
54pub enum MatrixElement {
55    /// [`<del>`], a deleted text element.
56    ///
57    /// [`<del>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del
58    Del,
59
60    /// [`<h1>-<h6>`], a section heading element.
61    ///
62    /// [`<h1>-<h6>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements
63    H(HeadingData),
64
65    /// [`<blockquote>`], a block quotation element.
66    ///
67    /// [`<blockquote>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote
68    Blockquote,
69
70    /// [`<p>`], a paragraph element.
71    ///
72    /// [`<p>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p
73    P,
74
75    /// [`<a>`], an anchor element.
76    ///
77    /// [`<a>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a
78    A(AnchorData),
79
80    /// [`<ul>`], an unordered list element.
81    ///
82    /// [`<ul>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul
83    Ul,
84
85    /// [`<ol>`], an ordered list element.
86    ///
87    /// [`<ol>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol
88    Ol(OrderedListData),
89
90    /// [`<sup>`], a superscript element.
91    ///
92    /// [`<sup>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup
93    Sup,
94
95    /// [`<sub>`], a subscript element.
96    ///
97    /// [`<sub>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub
98    Sub,
99
100    /// [`<li>`], a list item element.
101    ///
102    /// [`<li>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li
103    Li,
104
105    /// [`<b>`], a bring attention to element.
106    ///
107    /// [`<b>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b
108    B,
109
110    /// [`<i>`], an idiomatic text element.
111    ///
112    /// [`<i>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i
113    I,
114
115    /// [`<u>`], an unarticulated annotation element.
116    ///
117    /// [`<u>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u
118    U,
119
120    /// [`<strong>`], a strong importance element.
121    ///
122    /// [`<strong>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong
123    Strong,
124
125    /// [`<em>`], an emphasis element.
126    ///
127    /// [`<em>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em
128    Em,
129
130    /// [`<s>`], a strikethrough element.
131    ///
132    /// [`<s>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s
133    S,
134
135    /// [`<code>`], an inline code element.
136    ///
137    /// [`<code>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code
138    Code(CodeData),
139
140    /// [`<hr>`], a thematic break element.
141    ///
142    /// [`<hr>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr
143    Hr,
144
145    /// [`<br>`], a line break element.
146    ///
147    /// [`<br>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br
148    Br,
149
150    /// [`<div>`], a content division element.
151    ///
152    /// [`<div>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
153    Div(DivData),
154
155    /// [`<table>`], a table element.
156    ///
157    /// [`<table>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table
158    Table,
159
160    /// [`<thead>`], a table head element.
161    ///
162    /// [`<thead>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead
163    Thead,
164
165    /// [`<tbody>`], a table body element.
166    ///
167    /// [`<tbody>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody
168    Tbody,
169
170    /// [`<tr>`], a table row element.
171    ///
172    /// [`<tr>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr
173    Tr,
174
175    /// [`<th>`], a table header element.
176    ///
177    /// [`<th>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th
178    Th,
179
180    /// [`<td>`], a table data cell element.
181    ///
182    /// [`<td>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td
183    Td,
184
185    /// [`<caption>`], a table caption element.
186    ///
187    /// [`<caption>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption
188    Caption,
189
190    /// [`<pre>`], a preformatted text element.
191    ///
192    /// [`<pre>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre
193    Pre,
194
195    /// [`<span>`], a content span element.
196    ///
197    /// [`<span>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span
198    Span(SpanData),
199
200    /// [`<img>`], an image embed element.
201    ///
202    /// [`<img>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img
203    Img(ImageData),
204
205    /// [`<details>`], a details disclosure element.
206    ///
207    /// [`<details>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
208    Details,
209
210    /// [`<summary>`], a disclosure summary element.
211    ///
212    /// [`<summary>`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary
213    Summary,
214
215    /// [`mx-reply`], a Matrix rich reply fallback element.
216    ///
217    /// [`mx-reply`]: https://spec.matrix.org/latest/client-server-api/#rich-replies
218    MatrixReply,
219
220    /// An HTML element that is not in the suggested list.
221    Other(QualName),
222}
223
224impl MatrixElement {
225    /// Parse a `MatrixElement` from the given qualified name and attributes.
226    ///
227    /// Returns a tuple containing the constructed `Element` and the list of remaining unsupported
228    /// attributes.
229    #[allow(clippy::mutable_key_type)]
230    fn parse(name: &QualName, attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) {
231        if name.ns != ns!(html) {
232            return (Self::Other(name.clone()), attrs.clone());
233        }
234
235        match name.local.as_bytes() {
236            b"del" => (Self::Del, attrs.clone()),
237            b"h1" => (Self::H(HeadingData::new(1)), attrs.clone()),
238            b"h2" => (Self::H(HeadingData::new(2)), attrs.clone()),
239            b"h3" => (Self::H(HeadingData::new(3)), attrs.clone()),
240            b"h4" => (Self::H(HeadingData::new(4)), attrs.clone()),
241            b"h5" => (Self::H(HeadingData::new(5)), attrs.clone()),
242            b"h6" => (Self::H(HeadingData::new(6)), attrs.clone()),
243            b"blockquote" => (Self::Blockquote, attrs.clone()),
244            b"p" => (Self::P, attrs.clone()),
245            b"a" => {
246                let (data, attrs) = AnchorData::parse(attrs);
247                (Self::A(data), attrs)
248            }
249            b"ul" => (Self::Ul, attrs.clone()),
250            b"ol" => {
251                let (data, attrs) = OrderedListData::parse(attrs);
252                (Self::Ol(data), attrs)
253            }
254            b"sup" => (Self::Sup, attrs.clone()),
255            b"sub" => (Self::Sub, attrs.clone()),
256            b"li" => (Self::Li, attrs.clone()),
257            b"b" => (Self::B, attrs.clone()),
258            b"i" => (Self::I, attrs.clone()),
259            b"u" => (Self::U, attrs.clone()),
260            b"strong" => (Self::Strong, attrs.clone()),
261            b"em" => (Self::Em, attrs.clone()),
262            b"s" => (Self::S, attrs.clone()),
263            b"code" => {
264                let (data, attrs) = CodeData::parse(attrs);
265                (Self::Code(data), attrs)
266            }
267            b"hr" => (Self::Hr, attrs.clone()),
268            b"br" => (Self::Br, attrs.clone()),
269            b"div" => {
270                let (data, attrs) = DivData::parse(attrs);
271                (Self::Div(data), attrs)
272            }
273            b"table" => (Self::Table, attrs.clone()),
274            b"thead" => (Self::Thead, attrs.clone()),
275            b"tbody" => (Self::Tbody, attrs.clone()),
276            b"tr" => (Self::Tr, attrs.clone()),
277            b"th" => (Self::Th, attrs.clone()),
278            b"td" => (Self::Td, attrs.clone()),
279            b"caption" => (Self::Caption, attrs.clone()),
280            b"pre" => (Self::Pre, attrs.clone()),
281            b"span" => {
282                let (data, attrs) = SpanData::parse(attrs);
283                (Self::Span(data), attrs)
284            }
285            b"img" => {
286                let (data, attrs) = ImageData::parse(attrs);
287                (Self::Img(data), attrs)
288            }
289            b"details" => (Self::Details, attrs.clone()),
290            b"summary" => (Self::Summary, attrs.clone()),
291            b"mx-reply" => (Self::MatrixReply, attrs.clone()),
292            _ => (Self::Other(name.clone()), attrs.clone()),
293        }
294    }
295}
296
297/// The supported data of a `<h1>-<h6>` HTML element.
298#[derive(Debug, Clone)]
299#[non_exhaustive]
300pub struct HeadingData {
301    /// The level of the heading.
302    pub level: HeadingLevel,
303}
304
305impl HeadingData {
306    /// Constructs a new `HeadingData` with the given heading level.
307    fn new(level: u8) -> Self {
308        Self { level: HeadingLevel(level) }
309    }
310}
311
312/// The level of a heading element.
313///
314/// The supported levels range from 1 (highest) to 6 (lowest). Other levels cannot construct this
315/// and do not use the [`MatrixElement::H`] variant.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub struct HeadingLevel(u8);
318
319impl HeadingLevel {
320    /// The value of the level.
321    ///
322    /// Can only be a value between 1 and 6 included.
323    pub fn value(&self) -> u8 {
324        self.0
325    }
326}
327
328impl PartialEq<u8> for HeadingLevel {
329    fn eq(&self, other: &u8) -> bool {
330        self.0.eq(other)
331    }
332}
333
334/// The supported data of a `<a>` HTML element.
335#[derive(Debug, Clone)]
336#[non_exhaustive]
337pub struct AnchorData {
338    /// Where to display the linked URL.
339    pub target: Option<StrTendril>,
340
341    /// The URL that the hyperlink points to.
342    pub href: Option<AnchorUri>,
343}
344
345impl AnchorData {
346    /// Construct an empty `AnchorData`.
347    fn new() -> Self {
348        Self { target: None, href: None }
349    }
350
351    /// Parse the given attributes to construct a new `AnchorData`.
352    ///
353    /// Returns a tuple containing the constructed data and the remaining unsupported attributes.
354    #[allow(clippy::mutable_key_type)]
355    fn parse(attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) {
356        let mut data = Self::new();
357        let mut remaining_attrs = BTreeSet::new();
358
359        for attr in attrs {
360            if attr.name.ns != ns!() {
361                remaining_attrs.insert(attr.clone());
362                continue;
363            }
364
365            match attr.name.local.as_bytes() {
366                b"target" => {
367                    data.target = Some(attr.value.clone());
368                }
369                b"href" => {
370                    if let Some(uri) = AnchorUri::parse(&attr.value) {
371                        data.href = Some(uri);
372                    } else {
373                        remaining_attrs.insert(attr.clone());
374                    }
375                }
376                _ => {
377                    remaining_attrs.insert(attr.clone());
378                }
379            }
380        }
381
382        (data, remaining_attrs)
383    }
384}
385
386/// A URI as a value for the `href` attribute of a `<a>` HTML element.
387///
388/// This is a helper type that recognizes `matrix:` and `https://matrix.to` URIs to detect mentions.
389///
390/// If the URI is an invalid Matrix URI or does not use one of the suggested schemes, the `href`
391/// attribute will be in the `attrs` list of [`MatrixElementData`].
392#[derive(Debug, Clone)]
393#[non_exhaustive]
394pub enum AnchorUri {
395    /// A `matrix:` URI.
396    Matrix(MatrixUri),
397    /// A `https://matrix.to` URI.
398    MatrixTo(MatrixToUri),
399    /// An other URL using one of the suggested schemes.
400    ///
401    /// Those schemes are:
402    ///
403    /// * `https`
404    /// * `http`
405    /// * `ftp`
406    /// * `mailto`
407    /// * `magnet`
408    Other(StrTendril),
409}
410
411impl AnchorUri {
412    /// Parse the given string to construct a new `AnchorUri`.
413    fn parse(value: &StrTendril) -> Option<Self> {
414        let s = value.as_ref();
415
416        // Check if it starts with a supported scheme.
417        let mut allowed_schemes = spec::allowed_schemes("a", "href")
418            .into_iter()
419            .chain(compat::allowed_schemes("a", "href"))
420            .flatten();
421        if !allowed_schemes.any(|scheme| s.starts_with(&format!("{scheme}:"))) {
422            return None;
423        }
424
425        match MatrixUri::parse(s) {
426            Ok(uri) => return Some(Self::Matrix(uri)),
427            // It's not a `matrix:` URI, continue.
428            Err(IdParseError::InvalidMatrixUri(MatrixUriError::WrongScheme)) => {}
429            // The URI is invalid.
430            _ => return None,
431        }
432
433        match MatrixToUri::parse(s) {
434            Ok(uri) => return Some(Self::MatrixTo(uri)),
435            // It's not a `https://matrix.to` URI, continue.
436            Err(IdParseError::InvalidMatrixToUri(MatrixToError::WrongBaseUrl)) => {}
437            // The URI is invalid.
438            _ => return None,
439        }
440
441        Some(Self::Other(value.clone()))
442    }
443}
444
445/// The supported data of a `<ol>` HTML element.
446#[derive(Debug, Clone)]
447#[non_exhaustive]
448pub struct OrderedListData {
449    /// An integer to start counting from for the list items.
450    ///
451    /// If parsing the integer from a string fails, the attribute will be in the `attrs` list of
452    /// [`MatrixElementData`].
453    pub start: Option<i64>,
454}
455
456impl OrderedListData {
457    /// Construct an empty `OrderedListData`.
458    fn new() -> Self {
459        Self { start: None }
460    }
461
462    /// Parse the given attributes to construct a new `OrderedListData`.
463    ///
464    /// Returns a tuple containing the constructed data and the remaining unsupported attributes.
465    #[allow(clippy::mutable_key_type)]
466    fn parse(attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) {
467        let mut data = Self::new();
468        let mut remaining_attrs = BTreeSet::new();
469
470        for attr in attrs {
471            if attr.name.ns != ns!() {
472                remaining_attrs.insert(attr.clone());
473                continue;
474            }
475
476            match attr.name.local.as_bytes() {
477                b"start" => {
478                    if let Ok(start) = attr.value.parse() {
479                        data.start = Some(start);
480                    } else {
481                        remaining_attrs.insert(attr.clone());
482                    }
483                }
484                _ => {
485                    remaining_attrs.insert(attr.clone());
486                }
487            }
488        }
489
490        (data, remaining_attrs)
491    }
492}
493
494/// The supported data of a `<code>` HTML element.
495#[derive(Debug, Clone)]
496#[non_exhaustive]
497pub struct CodeData {
498    /// The language of the code, for syntax highlighting.
499    ///
500    /// This corresponds to the `class` attribute with a value that starts with the
501    /// `language-` prefix. The prefix is stripped from the value.
502    ///
503    /// If there are other classes in the `class` attribute, the whole attribute will be in the
504    /// `attrs` list of [`MatrixElementData`].
505    pub language: Option<StrTendril>,
506}
507
508impl CodeData {
509    /// Construct an empty `CodeData`.
510    fn new() -> Self {
511        Self { language: None }
512    }
513
514    /// Parse the given attributes to construct a new `CodeData`.
515    ///
516    /// Returns a tuple containing the constructed data and the remaining unsupported attributes.
517    #[allow(clippy::mutable_key_type)]
518    fn parse(attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) {
519        let mut data = Self::new();
520        let mut remaining_attrs = BTreeSet::new();
521
522        for attr in attrs {
523            if attr.name.ns != ns!() {
524                remaining_attrs.insert(attr.clone());
525                continue;
526            }
527
528            match attr.name.local.as_bytes() {
529                b"class" => {
530                    let value_str = attr.value.as_ref();
531
532                    // The attribute could contain several classes separated by spaces, so let's
533                    // find the first class starting with `language-`.
534                    for (match_start, _) in value_str.match_indices(CLASS_LANGUAGE_PREFIX) {
535                        // The class name must either be at the start of the string or preceded by a
536                        // space.
537                        if match_start != 0
538                            && !value_str.as_bytes()[match_start - 1].is_ascii_whitespace()
539                        {
540                            continue;
541                        }
542
543                        let language_start = match_start + CLASS_LANGUAGE_PREFIX.len();
544
545                        let str_end = &value_str[language_start..];
546                        let language_end = str_end
547                            .find(|c: char| c.is_ascii_whitespace())
548                            .map(|pos| language_start + pos)
549                            .unwrap_or(value_str.len());
550
551                        if language_end == language_start {
552                            continue;
553                        }
554
555                        let sub_len = (language_end - language_start) as u32;
556                        data.language = Some(attr.value.subtendril(language_start as u32, sub_len));
557
558                        if match_start != 0 || language_end != value_str.len() {
559                            // There are other classes, keep the whole attribute for the conversion
560                            // to be lossless.
561                            remaining_attrs.insert(attr.clone());
562                        }
563
564                        break;
565                    }
566
567                    if data.language.is_none() {
568                        // We didn't find the class we want, keep the whole attribute.
569                        remaining_attrs.insert(attr.clone());
570                    }
571                }
572                _ => {
573                    remaining_attrs.insert(attr.clone());
574                }
575            }
576        }
577
578        (data, remaining_attrs)
579    }
580}
581
582/// The supported data of a `<span>` HTML element.
583#[derive(Debug, Clone)]
584#[non_exhaustive]
585pub struct SpanData {
586    /// `data-mx-bg-color`, the background color of the text.
587    pub bg_color: Option<StrTendril>,
588
589    /// `data-mx-color`, the foreground color of the text.
590    pub color: Option<StrTendril>,
591
592    /// `data-mx-spoiler`, a Matrix [spoiler message].
593    ///
594    /// The value is the reason of the spoiler. If the string is empty, this is a spoiler
595    /// without a reason.
596    ///
597    /// [spoiler message]: https://spec.matrix.org/latest/client-server-api/#spoiler-messages
598    pub spoiler: Option<StrTendril>,
599
600    /// `data-mx-maths`, an inline Matrix [mathematical message].
601    ///
602    /// The value is the mathematical notation in [LaTeX] format.
603    ///
604    /// If this attribute is present, the content of the span is the fallback representation of the
605    /// mathematical notation.
606    ///
607    /// [mathematical message]: https://spec.matrix.org/latest/client-server-api/#mathematical-messages
608    /// [LaTeX]: https://www.latex-project.org/
609    pub maths: Option<StrTendril>,
610
611    /// `data-mx-external-payment-details`, unstable feature from MSC4186.
612    ///
613    /// This uses the unstable prefix in [MSC4286].
614    ///
615    /// [MSC4286]: https://github.com/matrix-org/matrix-spec-proposals/pull/4286
616    #[cfg(feature = "unstable-msc4286")]
617    pub external_payment_details: Option<StrTendril>,
618}
619
620impl SpanData {
621    /// Construct an empty `SpanData`.
622    fn new() -> Self {
623        Self {
624            bg_color: None,
625            color: None,
626            spoiler: None,
627            maths: None,
628            #[cfg(feature = "unstable-msc4286")]
629            external_payment_details: None,
630        }
631    }
632
633    /// Parse the given attributes to construct a new `SpanData`.
634    ///
635    /// Returns a tuple containing the constructed data and the remaining unsupported attributes.
636    #[allow(clippy::mutable_key_type)]
637    fn parse(attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) {
638        let mut data = Self::new();
639        let mut remaining_attrs = BTreeSet::new();
640
641        for attr in attrs {
642            if attr.name.ns != ns!() {
643                remaining_attrs.insert(attr.clone());
644                continue;
645            }
646
647            match attr.name.local.as_bytes() {
648                b"data-mx-bg-color" => {
649                    data.bg_color = Some(attr.value.clone());
650                }
651                b"data-mx-color" => data.color = Some(attr.value.clone()),
652                b"data-mx-spoiler" => {
653                    data.spoiler = Some(attr.value.clone());
654                }
655                b"data-mx-maths" => {
656                    data.maths = Some(attr.value.clone());
657                }
658                #[cfg(feature = "unstable-msc4286")]
659                b"data-msc4286-external-payment-details" => {
660                    data.external_payment_details = Some(attr.value.clone());
661                }
662                _ => {
663                    remaining_attrs.insert(attr.clone());
664                }
665            }
666        }
667
668        (data, remaining_attrs)
669    }
670}
671
672/// The supported data of a `<img>` HTML element.
673#[derive(Debug, Clone)]
674#[non_exhaustive]
675pub struct ImageData {
676    /// The intrinsic width of the image, in pixels.
677    ///
678    /// If parsing the integer from a string fails, the attribute will be in the `attrs` list of
679    /// `MatrixElementData`.
680    pub width: Option<i64>,
681
682    /// The intrinsic height of the image, in pixels.
683    ///
684    /// If parsing the integer from a string fails, the attribute will be in the `attrs` list of
685    /// [`MatrixElementData`].
686    pub height: Option<i64>,
687
688    /// Text that can replace the image.
689    pub alt: Option<StrTendril>,
690
691    ///  Text representing advisory information about the image.
692    pub title: Option<StrTendril>,
693
694    /// The image URL.
695    ///
696    /// It this is not a valid `mxc:` URI, the attribute will be in the `attrs` list of
697    /// [`MatrixElementData`].
698    pub src: Option<OwnedMxcUri>,
699}
700
701impl ImageData {
702    /// Construct an empty `ImageData`.
703    fn new() -> Self {
704        Self { width: None, height: None, alt: None, title: None, src: None }
705    }
706
707    /// Parse the given attributes to construct a new `ImageData`.
708    ///
709    /// Returns a tuple containing the constructed data and the remaining unsupported attributes.
710    #[allow(clippy::mutable_key_type)]
711    fn parse(attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) {
712        let mut data = Self::new();
713        let mut remaining_attrs = BTreeSet::new();
714
715        for attr in attrs {
716            if attr.name.ns != ns!() {
717                remaining_attrs.insert(attr.clone());
718                continue;
719            }
720
721            match attr.name.local.as_bytes() {
722                b"width" => {
723                    if let Ok(width) = attr.value.parse() {
724                        data.width = Some(width);
725                    } else {
726                        remaining_attrs.insert(attr.clone());
727                    }
728                }
729                b"height" => {
730                    if let Ok(height) = attr.value.parse() {
731                        data.height = Some(height);
732                    } else {
733                        remaining_attrs.insert(attr.clone());
734                    }
735                }
736                b"alt" => data.alt = Some(attr.value.clone()),
737                b"title" => data.title = Some(attr.value.clone()),
738                b"src" => {
739                    let uri = <&MxcUri>::from(attr.value.as_ref());
740                    if uri.validate().is_ok() {
741                        data.src = Some(uri.to_owned());
742                    } else {
743                        remaining_attrs.insert(attr.clone());
744                    }
745                }
746                _ => {
747                    remaining_attrs.insert(attr.clone());
748                }
749            }
750        }
751
752        (data, remaining_attrs)
753    }
754}
755
756/// The supported data of a `<div>` HTML element.
757#[derive(Debug, Clone)]
758#[non_exhaustive]
759pub struct DivData {
760    /// `data-mx-maths`, a Matrix [mathematical message] block.
761    ///
762    /// The value is the mathematical notation in [LaTeX] format.
763    ///
764    /// If this attribute is present, the content of the div is the fallback representation of the
765    /// mathematical notation.
766    ///
767    /// [mathematical message]: https://spec.matrix.org/latest/client-server-api/#mathematical-messages
768    /// [LaTeX]: https://www.latex-project.org/
769    pub maths: Option<StrTendril>,
770}
771
772impl DivData {
773    /// Construct an empty `DivData`.
774    fn new() -> Self {
775        Self { maths: None }
776    }
777
778    /// Parse the given attributes to construct a new `SpanData`.
779    ///
780    /// Returns a tuple containing the constructed data and the remaining unsupported attributes.
781    #[allow(clippy::mutable_key_type)]
782    fn parse(attrs: &BTreeSet<Attribute>) -> (Self, BTreeSet<Attribute>) {
783        let mut data = Self::new();
784        let mut remaining_attrs = BTreeSet::new();
785
786        for attr in attrs {
787            if attr.name.ns != ns!() {
788                remaining_attrs.insert(attr.clone());
789                continue;
790            }
791
792            match attr.name.local.as_bytes() {
793                b"data-mx-maths" => {
794                    data.maths = Some(attr.value.clone());
795                }
796                _ => {
797                    remaining_attrs.insert(attr.clone());
798                }
799            }
800        }
801
802        (data, remaining_attrs)
803    }
804}