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}