Skip to main content

icu_capi/
bidi.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5#[diplomat::bridge]
6#[diplomat::abi_rename = "icu4x_{0}_mv1"]
7pub mod ffi {
8    use alloc::boxed::Box;
9    use alloc::vec::Vec;
10    use core::fmt::Write;
11
12    #[cfg(feature = "buffer_provider")]
13    use crate::unstable::{errors::ffi::DataError, provider::ffi::DataProvider};
14
15    #[non_exhaustive]
16    #[diplomat::rust_link(unicode_bidi::Direction, Enum)]
17    pub enum BidiDirection {
18        // This is an output type, so the default mostly impacts deferred initialization.
19        // We pick Ltr since the algorithm defaults to Ltr in the absence of other info.
20        #[diplomat::attr(auto, default)]
21        Ltr,
22        Rtl,
23        Mixed,
24    }
25
26    #[diplomat::opaque]
27    /// An ICU4X Bidi object, containing loaded bidi data
28    #[diplomat::rust_link(icu::properties::props::BidiClass, Struct)]
29    #[diplomat::attr(demo_gen, disable)] // TODO needs custom page
30    pub struct Bidi(pub icu_properties::CodePointMapData<icu_properties::props::BidiClass>);
31
32    impl Bidi {
33        /// Creates a new [`Bidi`] from locale data using compiled data.
34        #[diplomat::attr(auto, constructor)]
35        #[cfg(feature = "compiled_data")]
36        pub fn create() -> Box<Bidi> {
37            Box::new(Bidi(
38                icu_properties::CodePointMapData::new().static_to_owned(),
39            ))
40        }
41
42        /// Creates a new [`Bidi`] from locale data, and a particular data source.
43        #[diplomat::attr(all(supports = fallible_constructors, supports = named_constructors), named_constructor = "with_provider")]
44        #[cfg(feature = "buffer_provider")]
45        pub fn create_with_provider(provider: &DataProvider) -> Result<Box<Bidi>, DataError> {
46            Ok(Box::new(Bidi(
47                icu_properties::CodePointMapData::try_new_unstable(&provider.get_unstable()?)?,
48            )))
49        }
50        /// Use the data loaded in this object to process a string and calculate bidi information
51        ///
52        /// Takes in a Level for the default level, if it is an invalid value or None it will default to Auto.
53        ///
54        /// Returns nothing if `text` is invalid UTF-8.
55        #[diplomat::rust_link(unicode_bidi::BidiInfo::new_with_data_source, FnInStruct)]
56        #[diplomat::rust_link(
57            icu::properties::CodePointMapDataBorrowed::bidi_class,
58            FnInStruct,
59            hidden
60        )]
61        #[diplomat::attr(not(supports = utf8_strings), disable)]
62        #[diplomat::attr(*, rename = "for_text")]
63        pub fn for_text_utf8<'text>(
64            &self,
65            text: &'text DiplomatStr,
66            default_level: Option<u8>,
67        ) -> Option<Box<BidiInfo<'text>>> {
68            let text = core::str::from_utf8(text).ok()?;
69
70            Some(Box::new(BidiInfo(
71                unicode_bidi::BidiInfo::new_with_data_source(
72                    &self.0.as_borrowed(),
73                    text,
74                    default_level.and_then(|l| unicode_bidi::Level::new(l).ok()),
75                ),
76            )))
77        }
78
79        /// Use the data loaded in this object to process a string and calculate bidi information
80        ///
81        /// Takes in a Level for the default level, if it is an invalid value it will default to LTR
82        #[diplomat::rust_link(unicode_bidi::BidiInfo::new_with_data_source, FnInStruct)]
83        #[diplomat::rust_link(
84            icu::properties::CodePointMapDataBorrowed::bidi_class,
85            FnInStruct,
86            hidden
87        )]
88        // The only safe UTF-8 strings are those generated by the Diplomat layer in UTF-16 languages
89        #[diplomat::attr(supports = utf8_strings, disable)]
90        #[diplomat::attr(supports = utf16_strings, rename = "for_text")]
91        pub fn for_text_valid_utf8<'text>(
92            &self,
93            text: &'text str,
94            default_level: Option<u8>,
95        ) -> Box<BidiInfo<'text>> {
96            Box::new(BidiInfo(unicode_bidi::BidiInfo::new_with_data_source(
97                &self.0.as_borrowed(),
98                text,
99                default_level.and_then(|l| unicode_bidi::Level::new(l).ok()),
100            )))
101        }
102
103        /// Utility function for producing reorderings given a list of levels
104        ///
105        /// Produces a map saying which visual index maps to which source index.
106        ///
107        /// The levels array must not have values greater than 126 (this is the
108        /// Bidi maximum explicit depth plus one).
109        /// Failure to follow this invariant may lead to incorrect results,
110        /// but is still safe.
111        #[diplomat::rust_link(unicode_bidi::BidiInfo::reorder_visual, FnInStruct)]
112        pub fn reorder_visual(&self, levels: &[u8]) -> Box<ReorderedIndexMap> {
113            let levels = unicode_bidi::Level::from_slice_unchecked(levels);
114            Box::new(ReorderedIndexMap(unicode_bidi::BidiInfo::reorder_visual(
115                levels,
116            )))
117        }
118
119        /// Check if a Level returned by level_at is an RTL level.
120        ///
121        /// Invalid levels (numbers greater than 125) will be assumed LTR
122        #[diplomat::rust_link(unicode_bidi::level::Level::is_rtl, FnInStruct)]
123        pub fn level_is_rtl(level: u8) -> bool {
124            unicode_bidi::Level::new(level)
125                .unwrap_or_else(|_| unicode_bidi::Level::ltr())
126                .is_rtl()
127        }
128
129        /// Check if a Level returned by level_at is an LTR level.
130        ///
131        /// Invalid levels (numbers greater than 125) will be assumed LTR
132        #[diplomat::rust_link(unicode_bidi::level::Level::is_ltr, FnInStruct)]
133        pub fn level_is_ltr(level: u8) -> bool {
134            unicode_bidi::Level::new(level)
135                .unwrap_or_else(|_| unicode_bidi::Level::ltr())
136                .is_ltr()
137        }
138
139        /// Get a basic RTL Level value
140        #[diplomat::rust_link(unicode_bidi::level::Level::rtl, FnInStruct)]
141        pub fn level_rtl() -> u8 {
142            unicode_bidi::Level::rtl().number()
143        }
144
145        /// Get a simple LTR Level value
146        #[diplomat::rust_link(unicode_bidi::level::Level::ltr, FnInStruct)]
147        pub fn level_ltr() -> u8 {
148            unicode_bidi::Level::ltr().number()
149        }
150    }
151
152    /// Thin wrapper around a vector that maps visual indices to source indices
153    ///
154    /// `map[visualIndex] = sourceIndex`
155    ///
156    /// Produced by `reorder_visual()` on [`Bidi`].
157    #[diplomat::opaque]
158    #[diplomat::attr(demo_gen, disable)] // TODO needs custom page
159    pub struct ReorderedIndexMap(pub Vec<usize>);
160
161    impl ReorderedIndexMap {
162        /// Get this as a slice/array of indices
163        #[diplomat::attr(auto, getter)]
164        pub fn as_slice<'a>(&'a self) -> &'a [usize] {
165            &self.0
166        }
167
168        /// The length of this map
169        #[diplomat::attr(auto, getter = "length")]
170        pub fn len(&self) -> usize {
171            self.0.len()
172        }
173
174        /// Whether this map is empty
175        #[diplomat::attr(auto, getter)]
176        pub fn is_empty(&self) -> bool {
177            self.0.is_empty()
178        }
179
180        /// Get element at `index`. Returns 0 when out of bounds
181        /// (note that 0 is also a valid in-bounds value, please use `len()`
182        /// to avoid out-of-bounds)
183        #[diplomat::attr(auto, indexer)]
184        pub fn get(&self, index: usize) -> usize {
185            self.0.get(index).copied().unwrap_or(0)
186        }
187    }
188
189    /// An object containing bidi information for a given string, produced by `for_text()` on `Bidi`
190    #[diplomat::rust_link(unicode_bidi::BidiInfo, Struct)]
191    #[diplomat::opaque]
192    #[diplomat::attr(demo_gen, disable)] // TODO needs custom page
193    pub struct BidiInfo<'text>(pub unicode_bidi::BidiInfo<'text>);
194
195    impl<'text> BidiInfo<'text> {
196        /// The number of paragraphs contained here
197        #[diplomat::attr(auto, getter)]
198        pub fn paragraph_count(&self) -> usize {
199            self.0.paragraphs.len()
200        }
201
202        /// Get the nth paragraph, returning `None` if out of bounds
203        pub fn paragraph_at(&'text self, n: usize) -> Option<Box<BidiParagraph<'text>>> {
204            self.0
205                .paragraphs
206                .get(n)
207                .map(|p| Box::new(BidiParagraph(unicode_bidi::Paragraph::new(&self.0, p))))
208        }
209
210        /// The number of bytes in this full text
211        #[diplomat::attr(auto, getter)]
212        pub fn size(&self) -> usize {
213            self.0.levels.len()
214        }
215
216        /// Get the BIDI level at a particular byte index in the full text.
217        /// This integer is conceptually a `unicode_bidi::Level`,
218        /// and can be further inspected using the static methods on Bidi.
219        ///
220        /// Returns 0 (equivalent to `Level::ltr()`) on error
221        pub fn level_at(&self, pos: usize) -> u8 {
222            if let Some(l) = self.0.levels.get(pos) {
223                l.number()
224            } else {
225                0
226            }
227        }
228    }
229
230    /// Bidi information for a single processed paragraph
231    #[diplomat::opaque]
232    #[diplomat::attr(demo_gen, disable)] // TODO needs custom page
233    pub struct BidiParagraph<'info>(pub unicode_bidi::Paragraph<'info, 'info>);
234
235    impl<'info> BidiParagraph<'info> {
236        /// Given a paragraph index `n` within the surrounding text, this sets this
237        /// object to the paragraph at that index. Returns nothing when out of bounds.
238        ///
239        /// This is equivalent to calling `paragraph_at()` on `BidiInfo` but doesn't
240        /// create a new object
241        pub fn set_paragraph_in_text(&mut self, n: usize) -> bool {
242            let Some(para) = self.0.info.paragraphs.get(n) else {
243                return false;
244            };
245            self.0 = unicode_bidi::Paragraph::new(self.0.info, para);
246            true
247        }
248
249        #[diplomat::rust_link(unicode_bidi::Paragraph::level_at, FnInStruct)]
250        #[diplomat::attr(auto, getter)]
251        /// The primary direction of this paragraph
252        pub fn direction(&self) -> BidiDirection {
253            self.0.direction().into()
254        }
255
256        /// The number of bytes in this paragraph
257        #[diplomat::rust_link(unicode_bidi::ParagraphInfo::len, FnInStruct)]
258        #[diplomat::attr(auto, getter)]
259        pub fn size(&self) -> usize {
260            self.0.para.len()
261        }
262
263        /// The start index of this paragraph within the source text
264        #[diplomat::attr(auto, getter)]
265        pub fn range_start(&self) -> usize {
266            self.0.para.range.start
267        }
268
269        /// The end index of this paragraph within the source text
270        #[diplomat::attr(auto, getter)]
271        pub fn range_end(&self) -> usize {
272            self.0.para.range.end
273        }
274
275        /// Reorder a line based on display order. The ranges are specified relative to the source text and must be contained
276        /// within this paragraph's range.
277        #[diplomat::rust_link(unicode_bidi::Paragraph::level_at, FnInStruct)]
278        #[diplomat::attr(demo_gen, disable)] // TODO needs custom page
279        pub fn reorder_line(
280            &self,
281            range_start: usize,
282            range_end: usize,
283            out: &mut DiplomatWrite,
284        ) -> Option<()> {
285            if range_start < self.range_start() || range_end > self.range_end() {
286                return None;
287            }
288
289            let info = self.0.info;
290            let para = self.0.para;
291
292            let reordered = info.reorder_line(para, range_start..range_end);
293
294            let _infallible = out.write_str(&reordered);
295
296            Some(())
297        }
298
299        /// Get the BIDI level at a particular byte index in this paragraph.
300        /// This integer is conceptually a `unicode_bidi::Level`,
301        /// and can be further inspected using the static methods on Bidi.
302        ///
303        /// Returns 0 (equivalent to `Level::ltr()`) on error
304        #[diplomat::rust_link(unicode_bidi::Paragraph::level_at, FnInStruct)]
305        pub fn level_at(&self, pos: usize) -> u8 {
306            if pos >= self.size() {
307                return 0;
308            }
309
310            self.0.level_at(pos).number()
311        }
312    }
313}
314
315use unicode_bidi::Direction;
316
317impl From<Direction> for ffi::BidiDirection {
318    fn from(other: Direction) -> Self {
319        match other {
320            Direction::Ltr => Self::Ltr,
321            Direction::Rtl => Self::Rtl,
322            Direction::Mixed => Self::Mixed,
323        }
324    }
325}