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}