brink_format/line.rs
1/// The content of a single output line — either a plain string or a template
2/// with interpolation slots and plural selects.
3#[derive(Debug, Clone, PartialEq)]
4pub enum LineContent {
5 Plain(String),
6 Template(LineTemplate),
7}
8
9bitflags::bitflags! {
10 /// Whitespace characteristics of a line, precomputed at compile time.
11 ///
12 /// Used by the output buffer to make filtering decisions (suppress leading
13 /// whitespace, collapse adjacent whitespace) without eagerly resolving
14 /// deferred `LineRef` parts.
15 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
16 pub struct LineFlags: u8 {
17 /// The resolved content starts with whitespace.
18 const STARTS_WITH_WS = 0b0001;
19 /// The resolved content ends with whitespace.
20 const ENDS_WITH_WS = 0b0010;
21 /// The resolved content is entirely whitespace (but not empty).
22 const ALL_WS = 0b0100;
23 /// The resolved content is empty.
24 const EMPTY = 0b1000;
25 }
26}
27
28impl LineFlags {
29 /// Compute flags from a `LineContent`.
30 ///
31 /// For `Plain` content, flags are exact. For `Template` content, flags
32 /// are conservative: `Slot`/`Select` parts are assumed to produce
33 /// non-whitespace content.
34 pub fn from_content(content: &LineContent) -> Self {
35 match content {
36 LineContent::Plain(s) => Self::from_plain(s),
37 LineContent::Template(parts) => Self::from_template(parts),
38 }
39 }
40
41 /// Compute flags from a plain string.
42 pub fn from_plain(s: &str) -> Self {
43 if s.is_empty() {
44 return Self::EMPTY;
45 }
46 let mut flags = Self::empty();
47 if s.starts_with(char::is_whitespace) {
48 flags |= Self::STARTS_WITH_WS;
49 }
50 if s.ends_with(char::is_whitespace) {
51 flags |= Self::ENDS_WITH_WS;
52 }
53 if s.trim().is_empty() {
54 flags |= Self::ALL_WS;
55 }
56 flags
57 }
58
59 fn from_template(parts: &[LinePart]) -> Self {
60 if parts.is_empty() {
61 return Self::EMPTY;
62 }
63 let mut flags = Self::empty();
64
65 // Check first part for leading whitespace.
66 if let Some(LinePart::Literal(s)) = parts.first()
67 && s.starts_with(char::is_whitespace)
68 {
69 flags |= Self::STARTS_WITH_WS;
70 }
71
72 // Check last part for trailing whitespace.
73 match parts.last() {
74 Some(LinePart::Literal(s)) if s.ends_with(char::is_whitespace) => {
75 flags |= Self::ENDS_WITH_WS;
76 }
77 _ => {}
78 }
79
80 // ALL_WS: only true if every part is whitespace-only literals.
81 // Any Slot/Select means we can't guarantee all-whitespace.
82 let all_ws = parts.iter().all(|p| match p {
83 LinePart::Literal(s) => s.trim().is_empty(),
84 _ => false,
85 });
86 if all_ws {
87 flags |= Self::ALL_WS;
88 }
89
90 flags
91 }
92}
93
94/// A sequence of literal and dynamic parts that compose an output line.
95pub type LineTemplate = Vec<LinePart>;
96
97/// One segment of a [`LineTemplate`].
98#[derive(Debug, Clone, PartialEq)]
99pub enum LinePart {
100 /// A literal string fragment.
101 Literal(String),
102 /// A value interpolation slot (index into the evaluation stack snapshot).
103 Slot(u8),
104 /// A plural/keyword select over a slot value.
105 Select {
106 slot: u8,
107 variants: Vec<(SelectKey, String)>,
108 default: String,
109 },
110}
111
112/// The key for matching a branch in a [`LinePart::Select`].
113#[derive(Debug, Clone, PartialEq)]
114pub enum SelectKey {
115 Cardinal(PluralCategory),
116 Ordinal(PluralCategory),
117 Exact(i32),
118 Keyword(String),
119}
120
121/// CLDR plural category.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum PluralCategory {
124 Zero,
125 One,
126 Two,
127 Few,
128 Many,
129 Other,
130}
131
132/// Trait for resolving plural categories at runtime.
133///
134/// Implementors provide locale-aware plural resolution. The `brink-intl` crate
135/// ships a batteries-included implementation backed by ICU4X baked data.
136pub trait PluralResolver {
137 /// Resolve the cardinal plural category for the given integer.
138 ///
139 /// `locale_override` allows overriding the resolver's default locale.
140 fn cardinal(&self, n: i64, locale_override: Option<&str>) -> PluralCategory;
141
142 /// Resolve the ordinal plural category for the given integer.
143 fn ordinal(&self, n: i64) -> PluralCategory;
144}