Skip to main content

imessage_database/tables/messages/models/
attributed_range.rs

1/*!
2 Attribute runs of a message body's `NSAttributedString`.
3*/
4
5use crate::{
6    message_types::text_effects::text_effect::TextEffect,
7    tables::messages::models::attachment_meta::AttachmentMeta,
8};
9
10/// One attribute run of a message's [`NSAttributedString`](crate::util::typedstream)
11/// body: a byte range into the [`Message`](crate::tables::messages::Message)'s [`text`](crate::tables::messages::Message::text)
12/// plus every attribute applied to it.
13///
14/// A range is a *text* range when [`attachment`](Self::attachment) is `None` and
15/// an *attachment* range (a `\u{FFFC}` placeholder for an inline attachment)
16/// when it is `Some`. Effects, styles, and the inline-emoji hint apply to either
17/// kind. The [`typedstream`](crate::util::typedstream) attribute dictionary is a flat bag, so an attachment
18/// range can also carry, say, an [`Animated`](TextEffect::Animated) effect.
19///
20/// Ranges that share a `__kIMMessagePartAttributeName` index are grouped into one
21/// [`BubbleComponent::Run`](crate::tables::messages::models::BubbleComponent::Run). For example, message text with a
22/// [`Mention`](TextEffect::Mention) like:
23///
24/// ```
25/// let message_text = "What's up, Christopher?";
26/// ```
27///
28/// parses into a single run of 3 ranges:
29///
30/// ```
31/// use imessage_database::message_types::text_effects::text_effect::TextEffect;
32/// use imessage_database::tables::messages::models::{AttributedRange, BubbleComponent};
33///
34/// let result = vec![BubbleComponent::Run(vec![
35///     AttributedRange::text(0, 11, vec![TextEffect::Default]),  // `What's up, `
36///     AttributedRange::text(11, 22, vec![TextEffect::Mention("+5558675309".to_string())]), // `Christopher`
37///     AttributedRange::text(22, 23, vec![TextEffect::Default])  // `?`
38/// ])];
39/// ```
40#[derive(Debug, PartialEq, Clone)]
41pub struct AttributedRange {
42    /// Start byte index in the message text.
43    pub start: usize,
44    /// End byte index in the message text.
45    pub end: usize,
46    /// Effects applied to this range.
47    pub effects: Vec<TextEffect>,
48    /// `Some` when this range is a `\u{FFFC}` placeholder for an attachment.
49    /// The attachment's metadata travels here; effects still apply alongside.
50    pub attachment: Option<AttachmentMeta>,
51    /// `true` when the range carries `__kIMEmojiImageAttributeName`–Apple's
52    /// hint to render the attachment inline–like an emoji (observed on
53    /// genmoji, Memoji, and custom sticker ranges).
54    pub emoji_image: bool,
55}
56
57impl AttributedRange {
58    /// Build a text range (no attachment, no inline-emoji hint) with the
59    /// specified start index, end index, and text effects.
60    #[must_use]
61    pub fn text(start: usize, end: usize, effects: Vec<TextEffect>) -> Self {
62        Self {
63            start,
64            end,
65            effects,
66            attachment: None,
67            emoji_image: false,
68        }
69    }
70
71    /// Build an attachment range carrying the given [`AttachmentMeta`], with
72    /// no inline-emoji hint.
73    #[must_use]
74    pub fn attachment(start: usize, end: usize, meta: AttachmentMeta) -> Self {
75        Self {
76            start,
77            end,
78            effects: vec![],
79            attachment: Some(meta),
80            emoji_image: false,
81        }
82    }
83
84    /// Build an inline-rendered attachment range, one Apple flagged with
85    /// `__kIMEmojiImageAttributeName` to render inline like an emoji (a Memoji,
86    /// genmoji, or custom sticker placed amongst text).
87    #[must_use]
88    pub fn inline_attachment(start: usize, end: usize, meta: AttachmentMeta) -> Self {
89        Self {
90            start,
91            end,
92            effects: vec![],
93            attachment: Some(meta),
94            emoji_image: true,
95        }
96    }
97
98    /// `true` when this range stands in for an attachment rather than text.
99    #[must_use]
100    pub fn is_attachment(&self) -> bool {
101        self.attachment.is_some()
102    }
103}