1use std::{
2 convert::Infallible,
3 fmt::{Debug, Display, Write},
4 ops::Range,
5 str::FromStr,
6};
7
8use crate::{
9 brush::Brush,
10 font::FontResource,
11 formatted_text::{FormattedTextBuilder, Run, RunSet},
12};
13
14#[derive(Debug, Clone)]
24pub struct BBCode {
25 pub text: String,
27 pub tags: Box<[BBTag]>,
29}
30
31#[derive(Clone, Eq, PartialEq)]
32pub struct BBTag {
33 pub position: usize,
38 pub data: BBTagData,
40}
41
42impl std::ops::Deref for BBTag {
43 type Target = BBTagData;
44
45 fn deref(&self) -> &Self::Target {
46 &self.data
47 }
48}
49
50impl Debug for BBTag {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 Display::fmt(&self, f)
53 }
54}
55
56impl Display for BBTag {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "{}@{}", self.data, self.position)
59 }
60}
61
62#[derive(Debug, Clone, Eq, PartialEq)]
64pub struct BBTagData {
65 pub label: String,
69 pub argument: Option<String>,
71 pub is_close: bool,
73}
74
75impl BBTagData {
76 pub fn open(label: String, argument: Option<String>) -> Self {
77 Self {
78 is_close: false,
79 label,
80 argument,
81 }
82 }
83 pub fn close(label: String, argument: Option<String>) -> Self {
84 Self {
85 is_close: true,
86 label,
87 argument,
88 }
89 }
90}
91
92impl Display for BBTagData {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.write_char('[')?;
95 if self.is_close {
96 f.write_char('/')?;
97 }
98 f.write_str(&self.label)?;
99 if let Some(arg) = &self.argument {
100 f.write_char('=')?;
101 f.write_str(arg)?;
102 }
103 f.write_char(']')
104 }
105}
106
107impl FromStr for BBTagData {
108 type Err = Infallible;
109 fn from_str(source: &str) -> Result<Self, Infallible> {
110 let mut source = source.as_bytes();
111 let mut is_close = false;
112 if let Some((b'/', rest)) = source.split_first() {
113 is_close = true;
114 source = rest;
115 }
116 if let Some(equals_pos) = source.iter().position(|c| *c == b'=') {
117 let (label, argument) = source.split_at(equals_pos);
118 let label = label.trim_ascii();
119 let argument = argument[1..].trim_ascii();
120 Ok(Self {
121 is_close,
122 label: std::str::from_utf8(label).unwrap().to_string(),
123 argument: Some(std::str::from_utf8(argument).unwrap().to_string()),
124 })
125 } else {
126 Ok(Self {
127 is_close,
128 label: std::str::from_utf8(source.trim_ascii())
129 .unwrap()
130 .to_string(),
131 argument: None,
132 })
133 }
134 }
135}
136
137impl FromStr for BBCode {
138 type Err = Infallible;
139 fn from_str(source: &str) -> Result<Self, Infallible> {
140 let mut source = source.as_bytes();
141 let mut text = Vec::new();
142 let mut tags = Vec::new();
143 while !source.is_empty() {
144 match source[0] {
145 b'[' => {
146 source = &source[1..];
147 if let Some(end_pos) = source.iter().position(|c| *c == b']') {
148 let content = std::str::from_utf8(&source[0..end_pos]).unwrap();
149 source = &source[end_pos + 1..];
150 let data: BBTagData = content.parse()?;
151 if !data.is_close && data.argument.is_none() && data.label == "br" {
152 text.push(b'\n');
153 } else {
154 tags.push(BBTag {
155 position: text.len(),
156 data,
157 });
158 }
159 } else {
160 source = &[];
161 }
162 }
163 c => {
164 text.push(c);
165 source = &source[1..];
166 }
167 }
168 }
169 Ok(Self {
170 text: std::str::from_utf8(&text).unwrap().to_string(),
171 tags: tags.into_boxed_slice(),
172 })
173 }
174}
175
176fn find_close<'a, I: Iterator<Item = &'a BBTag>>(label: &str, iter: I) -> Option<&'a BBTag> {
177 let mut nesting_level = 0;
178 for tag in iter {
179 if tag.is_close {
180 if nesting_level == 0 {
181 return (tag.label == label).then_some(tag);
182 } else {
183 nesting_level -= 1;
184 }
185 } else {
186 nesting_level += 1;
187 }
188 }
189 None
190}
191
192fn find_font_run(runs: &mut [Run], pos: u32) -> Option<&mut Run> {
193 runs.iter_mut()
194 .rev()
195 .find(|r| r.range.contains(&pos) && r.font().is_some())
196}
197
198fn update_font(
199 runs: &mut RunSet,
200 range: Range<u32>,
201 new_font: Option<&FontResource>,
202 other_font: Option<&FontResource>,
203 bold_italic: Option<&FontResource>,
204) {
205 if let Some(run) = find_font_run(runs, range.start) {
206 if other_font == run.font() {
207 if let Some(bold_italic) = bold_italic {
208 if range == run.range {
209 *run = Run::new(range).with_font(bold_italic.clone());
210 } else {
211 runs.push(Run::new(range).with_font(bold_italic.clone()));
212 }
213 }
214 }
215 } else if let Some(new_font) = new_font {
216 runs.push(Run::new(range).with_font(new_font.clone()));
217 }
218}
219
220fn apply_tag(
221 runs: &mut RunSet,
222 label: &str,
223 argument: Option<&str>,
224 range: Range<u32>,
225 font: &FontResource,
226) {
227 match (label, argument) {
228 ("i", None) => {
229 if font.is_ok() {
230 let font = font.data_ref();
231 update_font(
232 runs,
233 range,
234 font.italic.as_ref(),
235 font.bold.as_ref(),
236 font.bold_italic.as_ref(),
237 );
238 }
239 }
240 ("b", None) => {
241 if font.is_ok() {
242 let font = font.data_ref();
243 update_font(
244 runs,
245 range,
246 font.bold.as_ref(),
247 font.italic.as_ref(),
248 font.bold_italic.as_ref(),
249 );
250 }
251 }
252 ("size" | "s", Some(size)) => {
253 if let Ok(size) = size.parse() {
254 runs.push(Run::new(range).with_size(size));
255 }
256 }
257 ("color" | "c", Some(color)) => {
258 if let Ok(color) = color.parse() {
259 runs.push(Run::new(range).with_brush(Brush::Solid(color)));
260 }
261 }
262 ("shadow" | "sh", color) => {
263 let mut run = Run::new(range).with_shadow(true);
264 if let Some(color) = color.and_then(|c| c.parse().ok()) {
265 run = run.with_shadow_brush(Brush::Solid(color));
266 }
267 runs.push(run);
268 }
269 _ => (),
270 }
271}
272
273impl BBCode {
274 pub fn build_formatted_text(self, font: FontResource) -> FormattedTextBuilder {
275 let runs = self.build_runs(&font);
276 FormattedTextBuilder::new(font)
277 .with_text(self.text)
278 .with_runs(runs)
279 }
280 pub fn build_runs(&self, font: &FontResource) -> RunSet {
281 let mut runs = RunSet::default();
282 let mut iter = self.tags.iter();
283 while let Some(tag) = iter.next() {
284 if tag.is_close {
285 continue;
286 }
287 if let Some(close) = find_close(&tag.label, iter.clone()) {
288 let start_pos = self.text[0..tag.position].chars().count() as u32;
289 let end_pos = self.text[0..close.position].chars().count() as u32;
290 apply_tag(
291 &mut runs,
292 &tag.label,
293 tag.argument.as_deref(),
294 start_pos..end_pos,
295 font,
296 );
297 }
298 }
299 runs
300 }
301}
302
303#[cfg(test)]
304mod test {
305 use fyrox_core::color::Color;
306 use fyrox_resource::untyped::ResourceKind;
307 use uuid::Uuid;
308
309 use crate::font::{Font, BUILT_IN_FONT};
310
311 use super::*;
312 #[test]
313 fn test_built_in_font() {
314 let font = BUILT_IN_FONT.resource();
315 assert!(font.data_ref().bold.is_some());
316 assert!(font.data_ref().italic.is_some());
317 assert!(font.data_ref().bold_italic.is_some());
318 }
319 #[test]
320 fn test_example() {
321 let code: BBCode = "Here is [b]bold text[/b].".parse().unwrap();
322 assert_eq!(&code.text, "Here is bold text.");
323 assert_eq!(
324 *code.tags,
325 *&[
326 BBTag {
327 position: 8,
328 data: BBTagData::open("b".into(), None)
329 },
330 BBTag {
331 position: 17,
332 data: BBTagData::close("b".into(), None)
333 }
334 ]
335 );
336 }
337 #[test]
338 fn test_example2() {
339 let code: BBCode = "Here is [size = 24]big text[/ size= x ].".parse().unwrap();
340 assert_eq!(&code.text, "Here is big text.");
341 assert_eq!(
342 *code.tags,
343 *&[
344 BBTag {
345 position: 8,
346 data: BBTagData::open("size".into(), Some("24".into()))
347 },
348 BBTag {
349 position: 16,
350 data: BBTagData::close("size".into(), Some("x".into()))
351 }
352 ]
353 );
354 }
355 #[test]
356 fn test_formatted() {
357 let bold = FontResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, Font::default());
358 let italic = FontResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, Font::default());
359 let bold_italic =
360 FontResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, Font::default());
361 let font = FontResource::new_ok(
362 Uuid::new_v4(),
363 ResourceKind::Embedded,
364 Font {
365 bold: Some(bold.clone()),
366 italic: Some(italic.clone()),
367 bold_italic: Some(bold_italic.clone()),
368 ..Font::default()
369 },
370 );
371 let code: BBCode = "Here is [size=24]big text[/size].".parse().unwrap();
372 let text = code.build_formatted_text(font.clone()).build();
373 assert_eq!(**text.runs(), *&[Run::new(8..16).with_size(24.0)]);
374 let code: BBCode = "Here is [shadow]big text[/shadow].".parse().unwrap();
375 let text = code.build_formatted_text(font.clone()).build();
376 assert_eq!(**text.runs(), *&[Run::new(8..16).with_shadow(true)]);
377 let code: BBCode = "Here is [sh][s=24]big text[/s][/sh].".parse().unwrap();
378 let text = code.build_formatted_text(font.clone()).build();
379 assert_eq!(
380 **text.runs(),
381 *&[Run::new(8..16).with_shadow(true).with_size(24.0)]
382 );
383 let code: BBCode = "Here is [color=green]big text[/color].".parse().unwrap();
384 let text = code.build_formatted_text(font.clone()).build();
385 assert_eq!(
386 **text.runs(),
387 *&[Run::new(8..16).with_brush(Brush::Solid(Color::GREEN))]
388 );
389 let code: BBCode = "Here is [c=#010203]big text[/c].".parse().unwrap();
390 let text = code.build_formatted_text(font.clone()).build();
391 assert_eq!(
392 **text.runs(),
393 *&[Run::new(8..16).with_brush(Brush::Solid(Color::opaque(1, 2, 3)))]
394 );
395 let code: BBCode = "Here is [i]big text[/i].".parse().unwrap();
396 let text = code.build_formatted_text(font.clone()).build();
397 assert_eq!(**text.runs(), *&[Run::new(8..16).with_font(italic.clone())]);
398 let code: BBCode = "Here is [b]big text[/b].".parse().unwrap();
399 let text = code.build_formatted_text(font.clone()).build();
400 assert_eq!(**text.runs(), *&[Run::new(8..16).with_font(bold.clone())]);
401 let code: BBCode = "Here is [b][i]big text[/i][/b].".parse().unwrap();
402 let text = code.build_formatted_text(font.clone()).build();
403 assert_eq!(
404 **text.runs(),
405 *&[Run::new(8..16).with_font(bold_italic.clone())]
406 );
407 let code: BBCode = "Here is [i]big [b]text[/b]!!![/i]".parse().unwrap();
408 let text = code.build_formatted_text(font.clone()).build();
409 assert_eq!(
410 **text.runs(),
411 *&[
412 Run::new(8..19).with_font(italic.clone()),
413 Run::new(12..16).with_font(bold_italic.clone())
414 ]
415 );
416 }
417 #[test]
418 fn test_nesting() {
419 let font = FontResource::new_ok(Uuid::new_v4(), ResourceKind::Embedded, Font::default());
420 let code: BBCode = "Here is [s=24]big [s=3]small[/s] text[/s]."
421 .parse()
422 .unwrap();
423 let text = code.build_formatted_text(font.clone()).build();
424 assert_eq!(
425 **text.runs(),
426 *&[
427 Run::new(8..22).with_size(24.0),
428 Run::new(12..17).with_size(3.0)
429 ]
430 );
431 }
432}