1use alloc::string::String;
15use alloc::vec::Vec;
16
17use crate::{property_id, value_type};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum AttributedError {
22 InvalidRange,
24 RangeOutOfBounds,
26 TextTooLong,
28 Malformed,
30 InvalidUtf8,
32}
33
34impl core::fmt::Display for AttributedError {
35 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36 f.write_str(match self {
37 AttributedError::InvalidRange => "invalid attributed-text range",
38 AttributedError::RangeOutOfBounds => "attributed-text range out of bounds",
39 AttributedError::TextTooLong => "attributed text exceeds the u32 wire length",
40 AttributedError::Malformed => "malformed attributed-text payload",
41 AttributedError::InvalidUtf8 => "attributed text is not valid UTF-8",
42 })
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct TextAttribute {
49 pub property: u16,
51 pub value_type: u8,
53 pub value: u32,
55}
56
57impl TextAttribute {
58 #[must_use]
60 pub const fn new(property: u16, value_type: u8, value: u32) -> Self {
61 Self {
62 property,
63 value_type,
64 value,
65 }
66 }
67
68 #[must_use]
70 pub fn font_size(points: f32) -> Self {
71 Self::new(property_id::FONT_SIZE, value_type::F32, points.to_bits())
72 }
73
74 #[must_use]
76 pub fn font_weight(weight: f32) -> Self {
77 Self::new(property_id::FONT_WEIGHT, value_type::F32, weight.to_bits())
78 }
79
80 #[must_use]
82 pub const fn color(argb: u32) -> Self {
83 Self::new(property_id::COLOR, value_type::COLOR, argb)
84 }
85
86 #[must_use]
88 pub fn italic() -> Self {
89 Self::new(property_id::FONT_STYLE, value_type::F32, 1.0f32.to_bits())
90 }
91
92 #[must_use]
94 pub fn normal_style() -> Self {
95 Self::new(property_id::FONT_STYLE, value_type::F32, 0.0f32.to_bits())
96 }
97
98 #[must_use]
101 pub fn font_design(code: u8) -> Self {
102 Self::new(
103 property_id::FONT_DESIGN,
104 value_type::F32,
105 f32::from(code).to_bits(),
106 )
107 }
108
109 #[must_use]
111 pub const fn underline(on: bool) -> Self {
112 Self::new(property_id::UNDERLINE, value_type::U8, on as u32)
113 }
114
115 #[must_use]
117 pub const fn strikethrough(on: bool) -> Self {
118 Self::new(property_id::STRIKETHROUGH, value_type::U8, on as u32)
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct StyleRun {
125 pub start: u32,
127 pub end: u32,
129 pub attributes: Vec<TextAttribute>,
131}
132
133impl StyleRun {
134 #[must_use]
137 pub fn new(start: u32, end: u32) -> Self {
138 Self {
139 start,
140 end,
141 attributes: Vec::new(),
142 }
143 }
144
145 #[must_use]
147 pub fn with(mut self, attribute: TextAttribute) -> Self {
148 self.attributes.push(attribute);
149 self
150 }
151
152 #[must_use]
154 pub fn bold(self) -> Self {
155 self.with(TextAttribute::font_weight(700.0))
156 }
157
158 #[must_use]
160 pub fn italic(self) -> Self {
161 self.with(TextAttribute::italic())
162 }
163
164 #[must_use]
166 pub fn monospaced(self) -> Self {
167 self.with(TextAttribute::font_design(3))
168 }
169
170 #[must_use]
172 pub fn strikethrough(self) -> Self {
173 self.with(TextAttribute::strikethrough(true))
174 }
175
176 #[must_use]
178 pub fn underline(self) -> Self {
179 self.with(TextAttribute::underline(true))
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct LinkRun {
186 pub start: u32,
188 pub end: u32,
190 pub url: String,
192}
193
194impl LinkRun {
195 #[must_use]
197 pub fn new(start: u32, end: u32, url: impl Into<String>) -> Self {
198 Self {
199 start,
200 end,
201 url: url.into(),
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct AttributedSegment<'a> {
210 pub start: u32,
212 pub end: u32,
214 pub attributes: Vec<TextAttribute>,
216 pub link: Option<&'a str>,
218}
219
220#[derive(Debug, Clone, Default, PartialEq, Eq)]
222pub struct AttributedString {
223 text: String,
224 runs: Vec<StyleRun>,
225 links: Vec<LinkRun>,
226}
227
228impl AttributedString {
229 #[must_use]
231 pub fn plain(text: impl Into<String>) -> Self {
232 Self {
233 text: text.into(),
234 runs: Vec::new(),
235 links: Vec::new(),
236 }
237 }
238
239 pub fn new(
242 text: impl Into<String>,
243 runs: Vec<StyleRun>,
244 links: Vec<LinkRun>,
245 ) -> Result<Self, AttributedError> {
246 let value = Self {
247 text: text.into(),
248 runs,
249 links,
250 };
251 value.validate()?;
252 Ok(value)
253 }
254
255 #[must_use]
257 pub fn text(&self) -> &str {
258 &self.text
259 }
260
261 #[must_use]
263 pub fn runs(&self) -> &[StyleRun] {
264 &self.runs
265 }
266
267 #[must_use]
269 pub fn links(&self) -> &[LinkRun] {
270 &self.links
271 }
272
273 #[must_use]
275 pub fn is_plain(&self) -> bool {
276 self.runs.is_empty() && self.links.is_empty()
277 }
278
279 pub fn push_run(&mut self, run: StyleRun) -> Result<(), AttributedError> {
281 validate_range(&self.text, run.start, run.end)?;
282 self.runs.push(run);
283 Ok(())
284 }
285
286 pub fn push_link(&mut self, link: LinkRun) -> Result<(), AttributedError> {
288 validate_range(&self.text, link.start, link.end)?;
289 self.links.push(link);
290 Ok(())
291 }
292
293 pub fn validate(&self) -> Result<(), AttributedError> {
295 if self.text.len() > u32::MAX as usize {
296 return Err(AttributedError::TextTooLong);
297 }
298 for run in &self.runs {
299 validate_range(&self.text, run.start, run.end)?;
300 }
301 for link in &self.links {
302 validate_range(&self.text, link.start, link.end)?;
303 }
304 Ok(())
305 }
306
307 pub fn encode(&self) -> Result<Vec<u8>, AttributedError> {
311 self.validate()?;
312 let mut out = Vec::with_capacity(self.wire_len());
313 self.encode_into(&mut out)?;
314 Ok(out)
315 }
316
317 pub fn encode_into(&self, out: &mut Vec<u8>) -> Result<(), AttributedError> {
319 self.validate()?;
320 let text_len = u32::try_from(self.text.len()).map_err(|_| AttributedError::TextTooLong)?;
321 put_u32(out, text_len);
322 out.extend_from_slice(self.text.as_bytes());
323
324 put_u32(
325 out,
326 u32::try_from(self.runs.len()).map_err(|_| AttributedError::Malformed)?,
327 );
328 for run in &self.runs {
329 put_u32(out, run.start);
330 put_u32(out, run.end);
331 let supported: Vec<&TextAttribute> =
332 run.attributes.iter().filter(|a| is_supported(a)).collect();
333 put_u32(
334 out,
335 u32::try_from(supported.len()).map_err(|_| AttributedError::Malformed)?,
336 );
337 for attr in supported {
338 put_u16(out, attr.property);
339 out.push(attr.value_type);
340 out.push(0); put_u32(out, attr.value);
342 }
343 }
344
345 put_u32(
346 out,
347 u32::try_from(self.links.len()).map_err(|_| AttributedError::Malformed)?,
348 );
349 for link in &self.links {
350 put_u32(out, link.start);
351 put_u32(out, link.end);
352 put_u32(
353 out,
354 u32::try_from(link.url.len()).map_err(|_| AttributedError::Malformed)?,
355 );
356 out.extend_from_slice(link.url.as_bytes());
357 }
358 Ok(())
359 }
360
361 pub fn decode(bytes: &[u8]) -> Result<Self, AttributedError> {
363 let mut r = Reader::new(bytes);
364 let text_len = r.u32()? as usize;
365 let text_bytes = r.take(text_len)?;
366 let text = core::str::from_utf8(text_bytes).map_err(|_| AttributedError::InvalidUtf8)?;
367
368 let run_count = r.u32()?;
369 let mut runs = Vec::new();
370 for _ in 0..run_count {
371 let start = r.u32()?;
372 let end = r.u32()?;
373 let attr_count = r.u32()?;
374 validate_range(text, start, end)?;
375 let mut attributes = Vec::new();
376 for _ in 0..attr_count {
377 let property = r.u16()?;
378 let value_type = r.u8()?;
379 let _reserved = r.u8()?;
380 let value = r.u32()?;
381 let attr = TextAttribute::new(property, value_type, value);
382 if is_supported(&attr) {
383 attributes.push(attr);
384 }
385 }
386 runs.push(StyleRun {
387 start,
388 end,
389 attributes,
390 });
391 }
392
393 let link_count = r.u32()?;
394 let mut links = Vec::new();
395 for _ in 0..link_count {
396 let start = r.u32()?;
397 let end = r.u32()?;
398 let url_len = r.u32()? as usize;
399 validate_range(text, start, end)?;
400 let url =
401 core::str::from_utf8(r.take(url_len)?).map_err(|_| AttributedError::InvalidUtf8)?;
402 links.push(LinkRun {
403 start,
404 end,
405 url: url.into(),
406 });
407 }
408
409 if !r.is_empty() {
410 return Err(AttributedError::Malformed);
411 }
412 Ok(Self {
413 text: text.into(),
414 runs,
415 links,
416 })
417 }
418
419 #[must_use]
422 pub fn segments(&self) -> Vec<AttributedSegment<'_>> {
423 let mut boundaries: Vec<u32> =
424 Vec::with_capacity(2 + self.runs.len() * 2 + self.links.len() * 2);
425 boundaries.push(0);
426 boundaries.push(self.text.len() as u32);
427 for run in &self.runs {
428 boundaries.push(run.start);
429 boundaries.push(run.end);
430 }
431 for link in &self.links {
432 boundaries.push(link.start);
433 boundaries.push(link.end);
434 }
435 boundaries.sort_unstable();
436 boundaries.dedup();
437
438 let mut segments = Vec::with_capacity(boundaries.len().saturating_sub(1));
439 for pair in boundaries.windows(2) {
440 let (start, end) = (pair[0], pair[1]);
441 if start == end {
442 continue;
443 }
444 let mut attributes: Vec<TextAttribute> = Vec::new();
445 for run in &self.runs {
446 if run.start <= start && run.end >= end {
447 for attr in &run.attributes {
448 if let Some(existing) =
449 attributes.iter_mut().find(|a| a.property == attr.property)
450 {
451 *existing = *attr;
452 } else {
453 attributes.push(*attr);
454 }
455 }
456 }
457 }
458 let mut link = None;
459 for candidate in &self.links {
460 if candidate.start <= start && candidate.end >= end {
461 link = Some(candidate.url.as_str());
462 }
463 }
464 segments.push(AttributedSegment {
465 start,
466 end,
467 attributes,
468 link,
469 });
470 }
471 segments
472 }
473
474 fn wire_len(&self) -> usize {
475 let mut len = 4 + self.text.len() + 4;
476 for run in &self.runs {
477 len += 12;
478 len += run.attributes.iter().filter(|a| is_supported(a)).count() * 8;
479 }
480 len += 4;
481 for link in &self.links {
482 len += 12 + link.url.len();
483 }
484 len
485 }
486}
487
488impl From<&str> for AttributedString {
489 fn from(value: &str) -> Self {
490 Self::plain(value)
491 }
492}
493
494impl From<String> for AttributedString {
495 fn from(value: String) -> Self {
496 Self::plain(value)
497 }
498}
499
500#[must_use]
502pub fn is_supported(attr: &TextAttribute) -> bool {
503 match attr.property {
504 property_id::FONT_SIZE | property_id::FONT_WEIGHT => attr.value_type == value_type::F32,
505 property_id::FONT_STYLE | property_id::FONT_DESIGN => {
506 attr.value_type == value_type::F32 || attr.value_type == value_type::ENUM
507 }
508 property_id::COLOR => attr.value_type == value_type::COLOR,
509 property_id::UNDERLINE | property_id::STRIKETHROUGH => attr.value_type == value_type::U8,
510 _ => false,
511 }
512}
513
514fn validate_range(text: &str, start: u32, end: u32) -> Result<(), AttributedError> {
515 if start > end {
516 return Err(AttributedError::InvalidRange);
517 }
518 let (start, end) = (start as usize, end as usize);
519 if end > text.len() || !text.is_char_boundary(start) || !text.is_char_boundary(end) {
520 return Err(AttributedError::RangeOutOfBounds);
521 }
522 Ok(())
523}
524
525fn put_u16(out: &mut Vec<u8>, value: u16) {
526 out.extend_from_slice(&value.to_le_bytes());
527}
528
529fn put_u32(out: &mut Vec<u8>, value: u32) {
530 out.extend_from_slice(&value.to_le_bytes());
531}
532
533struct Reader<'a> {
534 bytes: &'a [u8],
535 pos: usize,
536}
537
538impl<'a> Reader<'a> {
539 fn new(bytes: &'a [u8]) -> Self {
540 Self { bytes, pos: 0 }
541 }
542
543 fn take(&mut self, len: usize) -> Result<&'a [u8], AttributedError> {
544 let end = self
545 .pos
546 .checked_add(len)
547 .ok_or(AttributedError::Malformed)?;
548 if end > self.bytes.len() {
549 return Err(AttributedError::Malformed);
550 }
551 let slice = &self.bytes[self.pos..end];
552 self.pos = end;
553 Ok(slice)
554 }
555
556 fn u8(&mut self) -> Result<u8, AttributedError> {
557 Ok(self.take(1)?[0])
558 }
559
560 fn u16(&mut self) -> Result<u16, AttributedError> {
561 let b = self.take(2)?;
562 Ok(u16::from_le_bytes([b[0], b[1]]))
563 }
564
565 fn u32(&mut self) -> Result<u32, AttributedError> {
566 let b = self.take(4)?;
567 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
568 }
569
570 fn is_empty(&self) -> bool {
571 self.pos == self.bytes.len()
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use alloc::vec;
579
580 fn bold_and_link() -> AttributedString {
581 AttributedString::new(
582 "Hi",
583 vec![StyleRun::new(0, 2).bold()],
584 vec![LinkRun::new(1, 2, "x")],
585 )
586 .unwrap()
587 }
588
589 #[test]
590 fn encodes_the_conformance_payload() {
591 let value = bold_and_link();
592 let bytes = value.encode().unwrap();
593 assert_eq!(
594 bytes,
595 vec![
596 0x02, 0x00, 0x00, 0x00, 0x48, 0x69, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x10, 0x04, 0x00, 0x00, 0x00, 0x2F, 0x44, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, ]
609 );
610 }
611
612 #[test]
613 fn round_trips() {
614 let value = bold_and_link();
615 assert_eq!(
616 AttributedString::decode(&value.encode().unwrap()).unwrap(),
617 value
618 );
619 }
620
621 #[test]
622 fn rejects_out_of_bounds_and_split_ranges() {
623 assert_eq!(
624 AttributedString::new("a", vec![StyleRun::new(0, 2)], vec![]),
625 Err(AttributedError::RangeOutOfBounds)
626 );
627 assert_eq!(
628 AttributedString::new("é", vec![StyleRun::new(1, 2)], vec![]),
629 Err(AttributedError::RangeOutOfBounds)
630 );
631 assert_eq!(
632 AttributedString::new("a", vec![StyleRun::new(1, 0)], vec![]),
633 Err(AttributedError::InvalidRange)
634 );
635 }
636
637 #[test]
638 fn ignores_unknown_attributes_and_truncated_payloads_fail() {
639 let mut value = bold_and_link();
640 value.runs[0]
641 .attributes
642 .push(TextAttribute::new(0x9999, value_type::F32, 0));
643 let bytes = value.encode().unwrap();
644 let decoded = AttributedString::decode(&bytes).unwrap();
645 assert_eq!(decoded.runs()[0].attributes.len(), 1);
646 assert_eq!(
647 AttributedString::decode(&bytes[..bytes.len() - 1]),
648 Err(AttributedError::Malformed)
649 );
650 }
651
652 #[test]
653 fn segments_resolve_overlaps_and_links() {
654 let value = AttributedString::new(
655 "abcd",
656 vec![
657 StyleRun::new(0, 3).bold(),
658 StyleRun::new(1, 4).italic(),
659 StyleRun::new(1, 2).with(TextAttribute::font_weight(400.0)),
660 ],
661 vec![LinkRun::new(2, 4, "x")],
662 )
663 .unwrap();
664 let segments = value.segments();
665 assert_eq!(segments.len(), 4);
666 assert_eq!((segments[0].start, segments[0].end), (0, 1));
667 assert_eq!(segments[0].attributes.len(), 1);
668 assert_eq!((segments[1].start, segments[1].end), (1, 2));
669 assert_eq!(segments[1].attributes.len(), 2);
670 assert!(segments[1]
671 .attributes
672 .iter()
673 .any(|a| a.property == property_id::FONT_WEIGHT && a.value == 400.0f32.to_bits()));
674 assert_eq!((segments[2].start, segments[2].end), (2, 3));
675 assert_eq!(segments[2].link, Some("x"));
676 assert_eq!((segments[3].start, segments[3].end), (3, 4));
677 assert_eq!(segments[3].link, Some("x"));
678 }
679
680 #[test]
681 fn rejects_trailing_bytes() {
682 let mut bytes = bold_and_link().encode().unwrap();
683 bytes.push(0);
684 assert_eq!(
685 AttributedString::decode(&bytes),
686 Err(AttributedError::Malformed)
687 );
688 }
689
690 #[test]
691 fn plain_text_is_empty_payload() {
692 assert!(AttributedString::plain("hi").is_plain());
693 assert_eq!(
694 AttributedString::decode(&AttributedString::plain("hi").encode().unwrap())
695 .unwrap()
696 .text(),
697 "hi"
698 );
699 }
700}