maplibre/legacy/
tagged_string.rs

1//! Translated from https://github.com/maplibre/maplibre-native/blob/4add9ea/src/mbgl/text/tagged_string.cpp
2
3use csscolorparser::Color;
4use widestring::{U16Str, U16String};
5
6use crate::legacy::{
7    bidi::{Char16, StyledText},
8    font_stack::{FontStack, FontStackHash, FontStackHasher},
9    util::{
10        i18n,
11        i18n::{BACKSLACK_F, BACKSLACK_V},
12    },
13};
14
15/// maplibre/maplibre-native#4add9ea original name: SectionOptions
16#[derive(Clone, Default)]
17pub struct SectionOptions {
18    pub scale: f64,
19    pub font_stack_hash: FontStackHash,
20    pub font_stack: FontStack,
21    pub text_color: Option<Color>,
22    pub image_id: Option<String>,
23}
24impl SectionOptions {
25    /// maplibre/maplibre-native#4add9ea original name: from_image_id
26    pub fn from_image_id(image_id: String) -> Self {
27        Self {
28            scale: 1.0,
29            image_id: Some(image_id),
30            ..SectionOptions::default()
31        }
32    }
33    /// maplibre/maplibre-native#4add9ea original name: new
34    pub fn new(scale: f64, font_stack: FontStack, text_color: Option<Color>) -> Self {
35        Self {
36            scale,
37            font_stack_hash: FontStackHasher::new(&font_stack),
38            font_stack,
39            text_color,
40            image_id: None,
41        }
42    }
43}
44
45const PUABEGIN: Char16 = '\u{E000}' as Char16;
46const PUAEND: Char16 = '\u{F8FF}' as Char16;
47
48/**
49 * A TaggedString is the shaping-code counterpart of the Formatted type
50 * Whereas Formatted matches the logical structure of a 'format' expression,
51 * a TaggedString represents the same data at a per-character level so that
52 * character-rearranging operations (e.g. BiDi) preserve formatting.
53 * Text is represented as:
54 * - A string of characters
55 * - A matching array of indices, pointing to:
56 * - An array of SectionsOptions, representing the evaluated formatting
57 *    options of the original sections.
58 *
59 * Once the guts of a TaggedString have been re-arranged by BiDi, you can
60 * iterate over the contents in order, using getCharCodeAt and getSection
61 * to get the formatting options for each character in turn.
62 */
63/// maplibre/maplibre-native#4add9ea original name: TaggedString
64#[derive(Clone)]
65pub struct TaggedString {
66    pub styled_text: StyledText,
67    pub sections: Vec<SectionOptions>,
68    pub supports_vertical_writing_mode: Option<bool>,
69    // Max number of images within a text is 6400 U+E000–U+F8FF
70    // that covers Basic Multilingual Plane Unicode Private Use Area (PUA).
71    pub image_section_id: Char16,
72}
73
74impl Default for TaggedString {
75    /// Returns an empty string
76    /// maplibre/maplibre-native#4add9ea original name: default
77    fn default() -> Self {
78        Self {
79            styled_text: (U16String::new(), vec![]), // TODO is this correct?
80            sections: vec![],
81            supports_vertical_writing_mode: None,
82            image_section_id: 0 as Char16, // TODO is this correct?
83        }
84    }
85}
86
87impl TaggedString {
88    /// maplibre/maplibre-native#4add9ea original name: new_from_raw
89    pub fn new_from_raw(text_: U16String, options: SectionOptions) -> Self {
90        let text_len = text_.len();
91        Self {
92            styled_text: (text_, vec![0; text_len]), // TODO is this correct?
93            sections: vec![options],
94            supports_vertical_writing_mode: None,
95            image_section_id: 0 as Char16, // TODO is this correct?
96        }
97    }
98
99    /// maplibre/maplibre-native#4add9ea original name: new
100    pub fn new(styled_text: StyledText, sections_: Vec<SectionOptions>) -> Self {
101        Self {
102            styled_text,
103            sections: sections_,
104            supports_vertical_writing_mode: None,
105            image_section_id: 0 as Char16, // TODO is this correct?
106        }
107    }
108
109    /// maplibre/maplibre-native#4add9ea original name: length
110    pub fn length(&self) -> usize {
111        self.styled_text.0.len()
112    }
113
114    /// maplibre/maplibre-native#4add9ea original name: sectionCount
115    pub fn section_count(&self) -> usize {
116        self.sections.len()
117    }
118
119    /// maplibre/maplibre-native#4add9ea original name: empty
120    pub fn empty(&self) -> bool {
121        self.styled_text.0.is_empty()
122    }
123
124    /// maplibre/maplibre-native#4add9ea original name: getSection
125    pub fn get_section(&self, index: usize) -> &SectionOptions {
126        &self.sections[self.styled_text.1[index] as usize] // TODO Index does not honor encoding, fine? previously it was .at()
127    }
128
129    /// maplibre/maplibre-native#4add9ea original name: getCharCodeAt
130    pub fn get_char_code_at(&self, index: usize) -> u16 {
131        return self.styled_text.0.as_slice()[index];
132    }
133
134    /// maplibre/maplibre-native#4add9ea original name: rawText
135    pub fn raw_text(&self) -> &U16String {
136        &self.styled_text.0
137    }
138
139    /// maplibre/maplibre-native#4add9ea original name: getStyledText
140    pub fn get_styled_text(&self) -> &StyledText {
141        &self.styled_text
142    }
143
144    /// maplibre/maplibre-native#4add9ea original name: addTextSection
145    pub fn add_text_section(
146        &mut self,
147        section_text: &U16String,
148        scale: f64,
149        font_stack: FontStack,
150        text_color: Option<Color>,
151    ) {
152        self.styled_text.0.push(section_text);
153        self.sections
154            .push(SectionOptions::new(scale, font_stack, text_color));
155        self.styled_text
156            .1
157            .resize(self.styled_text.0.len(), (self.sections.len() - 1) as u8);
158        self.supports_vertical_writing_mode = None;
159    }
160
161    /// maplibre/maplibre-native#4add9ea original name: addImageSection
162    pub fn add_image_section(&mut self, image_id: String) {
163        let next_image_section_char_code = self.get_next_image_section_char_code();
164
165        if let Some(next_image_section_char_code) = next_image_section_char_code {
166            self.styled_text
167                .0
168                .push(U16Str::from_slice(&[next_image_section_char_code])); // TODO is this correct?
169            self.sections.push(SectionOptions::from_image_id(image_id));
170            self.styled_text
171                .1
172                .resize(self.styled_text.0.len(), (self.sections.len() - 1) as u8);
173        } else {
174            log::warn!("Exceeded maximum number of images in a label.");
175        }
176    }
177
178    /// maplibre/maplibre-native#4add9ea original name: sectionAt
179    pub fn section_at(&self, index: usize) -> &SectionOptions {
180        &self.sections[index]
181    }
182
183    /// maplibre/maplibre-native#4add9ea original name: getSections
184    pub fn get_sections(&self) -> &Vec<SectionOptions> {
185        &self.sections
186    }
187
188    /// maplibre/maplibre-native#4add9ea original name: getSectionIndex
189    pub fn get_section_index(&self, character_index: usize) -> u8 {
190        self.styled_text.1[character_index] // TODO Index does not honor encoding, fine? previously it was .at()
191    }
192
193    /// maplibre/maplibre-native#4add9ea original name: getMaxScale
194    pub fn get_max_scale(&self) -> f64 {
195        let mut max_scale: f64 = 0.0;
196        for i in 0..self.styled_text.0.len() {
197            max_scale = max_scale.max(self.get_section(i).scale)
198        }
199        max_scale
200    }
201
202    const WHITESPACE_CHARS: &'static [Char16] = &[
203        ' ' as Char16,
204        '\t' as Char16,
205        '\n' as Char16,
206        BACKSLACK_V as Char16,
207        BACKSLACK_F as Char16,
208        '\r' as Char16,
209    ];
210
211    /// maplibre/maplibre-native#4add9ea original name: trim
212    pub fn trim(&mut self) {
213        let beginning_whitespace: Option<usize> = self
214            .styled_text
215            .0
216            .as_slice()
217            .iter()
218            .position(|c| !Self::WHITESPACE_CHARS.contains(c));
219
220        if let Some(beginning_whitespace) = beginning_whitespace {
221            let trailing_whitespace: usize = self
222                .styled_text
223                .0
224                .as_slice()
225                .iter()
226                .rposition(|c| !Self::WHITESPACE_CHARS.contains(c))
227                .expect("there is a whitespace char")
228                + 1;
229
230            self.styled_text.0 =
231                U16String::from(&self.styled_text.0[beginning_whitespace..trailing_whitespace]); // TODO write test for this
232            self.styled_text.1 =
233                Vec::from(&self.styled_text.1[beginning_whitespace..trailing_whitespace]);
234        } else {
235            // Entirely whitespace
236            self.styled_text.0.clear();
237            self.styled_text.1.clear();
238        }
239    }
240
241    /// maplibre/maplibre-native#4add9ea original name: verticalizePunctuation
242    pub fn verticalize_punctuation(&mut self) {
243        // Relies on verticalization changing characters in place so that style indices don't need updating
244        self.styled_text.0 = i18n::verticalize_punctuation_str(&self.styled_text.0);
245    }
246    /// maplibre/maplibre-native#4add9ea original name: allowsVerticalWritingMode
247    pub fn allows_vertical_writing_mode(&mut self) -> bool {
248        if self.supports_vertical_writing_mode.is_none() {
249            let new_value = i18n::allows_vertical_writing_mode(self.raw_text());
250            self.supports_vertical_writing_mode = Some(new_value);
251            return new_value;
252        }
253        self.supports_vertical_writing_mode
254            .expect("supportsVerticalWritingMode mut be set")
255    }
256}
257
258impl TaggedString {
259    /// maplibre/maplibre-native#4add9ea original name: getNextImageSectionCharCode
260    fn get_next_image_section_char_code(&mut self) -> Option<Char16> {
261        if self.image_section_id == 0 {
262            self.image_section_id = PUABEGIN;
263            return Some(self.image_section_id);
264        }
265
266        self.image_section_id += 1;
267        if self.image_section_id > PUAEND {
268            return None;
269        }
270
271        Some(self.image_section_id)
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use widestring::U16String;
278
279    use crate::legacy::{
280        bidi::Char16,
281        tagged_string::{SectionOptions, TaggedString},
282        util::i18n::BACKSLACK_V,
283    };
284
285    #[test]
286    /// maplibre/maplibre-native#4add9ea original name: TaggedString_Trim
287    fn tagged_string_trim() {
288        let mut basic = TaggedString::new_from_raw(
289            " \t\ntrim that and not this  \n\t".into(),
290            SectionOptions::new(1.0, vec![], None),
291        );
292        basic.trim();
293        assert_eq!(basic.raw_text(), &U16String::from("trim that and not this"));
294
295        let mut two_sections = TaggedString::default();
296        two_sections.add_text_section(&" \t\ntrim that".into(), 1.5, vec![], None);
297        two_sections.add_text_section(&" and not this  \n\t".into(), 0.5, vec![], None);
298
299        two_sections.trim();
300        assert_eq!(
301            two_sections.raw_text(),
302            &U16String::from("trim that and not this")
303        );
304
305        let mut empty = TaggedString::new_from_raw(
306            format!(
307                "\n\t{} \r  \t\n",
308                char::from_u32(BACKSLACK_V as u32).unwrap()
309            )
310            .into(),
311            SectionOptions::new(1.0, vec![], None),
312        );
313        empty.trim();
314        assert_eq!(empty.raw_text(), &U16String::from(""));
315
316        let mut no_trim =
317            TaggedString::new_from_raw("no trim!".into(), SectionOptions::new(1.0, vec![], None));
318        no_trim.trim();
319        assert_eq!(no_trim.raw_text(), &U16String::from("no trim!"));
320    }
321    #[test]
322    /// maplibre/maplibre-native#4add9ea original name: TaggedString_ImageSections
323    fn tagged_string_image_sections() {
324        let mut string = TaggedString::new_from_raw(U16String::new(), SectionOptions::default());
325        string.add_image_section("image_name".to_string());
326        assert_eq!(string.raw_text(), &U16String::from("\u{E000}"));
327        assert!(string.get_section(0).image_id.is_some());
328        assert_eq!(
329            string.get_section(0).image_id.as_ref().unwrap(),
330            &"image_name".to_string()
331        );
332
333        let mut max_sections = TaggedString::default();
334        for i in 0..6401 {
335            max_sections.add_image_section(i.to_string());
336        }
337
338        assert_eq!(max_sections.get_sections().len(), 6400);
339        assert_eq!(max_sections.get_char_code_at(0), '\u{E000}' as Char16);
340        assert_eq!(max_sections.get_char_code_at(6399), '\u{F8FF}' as Char16);
341    }
342}