1use std::sync::Arc;
2
3use regex::{Regex, Captures, Error};
4
5static AUTOLINKID: &str = "autolinker";
15static CONSUMETEXTID : &str = "consumetext";
16static NORMALTEXTID: &str = "normaltext";
17static BRNEWLINEID: &str = "convertnewlinebr";
18static NEWLINEID: &str = "newline";
19
20const BASICTAGS: &[&str] = &[
21 "b", "i", "sup", "sub", "u", "s", "list", r"\*", "url", "img"
22];
23const EXTENDEDTAGS: &[&str] = &[
24 "h1", "h2", "h3", "anchor", "quote", "spoiler", "icode", "code", "youtube"
25];
26
27pub type EmitScope = Arc<dyn Fn(Option<Captures>, &str, Option<Captures>)->String + Send + Sync>;
30pub type EmitSimple = Arc<dyn Fn(Captures)->String + Send + Sync>; pub struct ScopeInfo {
36 pub only: Option<Vec<&'static str>>,
39 pub double_closes: bool,
44 pub emit: EmitScope,
47}
48
49impl Default for ScopeInfo {
50 fn default() -> Self {
53 Self {
54 only: None,
55 double_closes: false,
56 emit: Arc::new(|_o, b, _c| String::from(b))
57 }
58 }
59}
60
61impl ScopeInfo {
62 pub fn basic(emitter: EmitScope) -> Self {
64 let mut result = Self::default();
65 result.emit = emitter;
66 result
67 }
68}
69
70pub enum MatchType {
74 Simple(EmitSimple),
76 Open(Arc<ScopeInfo>),
78 Close
80}
81
82pub struct MatchInfo {
86 pub id: &'static str,
88 pub regex : Regex,
90 pub match_type: MatchType,
92}
93
94struct BBScope<'a> {
100 id: &'static str,
102 info: Arc<ScopeInfo>,
105 open_tag_capture: Option<Captures<'a>>,
108 body: String,
112}
113
114impl BBScope<'_> {
115 fn is_allowed(&self, info: &MatchInfo) -> bool {
117 if let Some(only) = &self.info.only {
118 if self.id == info.id && matches!(info.match_type, MatchType::Close) {
120 return true;
121 }
122 for &only_str in only.iter() {
123 if only_str.starts_with("attr:") {
126 let only_name = &only_str[5..];
127 if only_name == info.id {
128 if let Some(ref capture) = self.open_tag_capture {
129 if capture.name("attr").is_some() {
130 return true;
131 }
132 }
133 }
134 }
135 else if only_str == info.id {
136 return true;
137 }
138 }
139 return false;
141 }
142 else {
144 true
145 }
146 }
147 fn closes(&self, id: &str) -> bool {
149 self.id == id && self.info.double_closes
150 }
151 fn emit(self, close_captures: Option<Captures>) -> String {
153 let emitter = &self.info.emit;
154 emitter(self.open_tag_capture, &self.body, close_captures)
155 }
156}
157
158struct BBScoper<'a> {
161 scopes : Vec<BBScope<'a>>
162}
163
164impl<'a> BBScoper<'a>
166{
167 fn new() -> Self {
170 Self {
171 scopes: vec![
172 BBScope {
173 id: "STARTING_SCOPE",
174 info: Arc::new(ScopeInfo::default()) ,
175 open_tag_capture: None,
176 body: String::new()
177 }
178 ]
179 }
180 }
181
182 fn is_allowed(&self, id: &MatchInfo) -> bool {
185 self.scopes.last().unwrap().is_allowed(id)
186 }
187
188 fn close_last(&mut self, close_tag: Option<Captures>) {
190 if let Some(scope) = self.scopes.pop() {
191 let body = scope.emit(close_tag); self.add_text(&body);
193 }
194 else {
195 panic!("BBScoper::close_last HOW DID THIS HAPPEN? There were scopes from .last but pop returned none!");
196 }
197 }
198
199 fn add_text(&mut self, text: &str) {
201 let mut last_scope = self.scopes.pop().unwrap();
203 last_scope.body.push_str(text);
204 self.scopes.push(last_scope);
205 }
206
207 fn add_char(&mut self, ch: char) {
209 let mut last_scope = self.scopes.pop().unwrap();
210 last_scope.body.push(ch);
211 self.scopes.push(last_scope);
212 }
213
214 fn add_scope(&mut self, scope: BBScope<'a>) {
217 if let Some(topinfo) = self.scopes.last() {
219 if topinfo.closes(scope.id) {
221 self.close_last(None);
222 }
223 }
224
225 self.scopes.push(scope);
226 }
227
228 fn close_scope(&mut self, id: &'static str) -> usize {
231 let mut scope_count = 0;
232 let mut tag_found : bool = false;
233
234 for scope in self.scopes.iter().rev() {
237 scope_count += 1;
238 if id == scope.id {
239 tag_found = true;
241 break;
242 }
243 }
244
245 if tag_found {
247 for _i in 0..scope_count {
248 self.close_last(None);
249 }
250 scope_count
251 }
252 else {
253 0 }
255 }
256
257 fn dump_remaining(mut self) -> String {
259 while self.scopes.len() > 1 {
260 self.close_last(None)
261 }
262 self.scopes.pop().unwrap().emit(None)
263 }
264}
265
266
267#[derive(Clone, Debug, Default)]
273pub enum BBCodeLinkTarget {
274 None,
276 #[default]
278 Blank
279}
280
281#[derive(Clone, Debug)]
285pub struct BBCodeTagConfig {
286 pub link_target: BBCodeLinkTarget,
287 pub img_in_url: bool,
288 pub newline_to_br: bool,
289 pub accepted_tags: Vec<String>,
290 }
292
293impl Default for BBCodeTagConfig {
294 fn default() -> Self {
297 Self {
298 link_target: BBCodeLinkTarget::default(),
299 img_in_url: true,
300 newline_to_br: true,
301 accepted_tags: BASICTAGS.iter().map(|t| t.to_string()).collect(),
302 }
304 }
305}
306
307impl BBCodeTagConfig {
308 pub fn extended() -> Self {
310 let mut config = BBCodeTagConfig::default();
311 let mut extags : Vec<String> = EXTENDEDTAGS.iter().map(|t| t.to_string()).collect();
312 config.accepted_tags.append(&mut extags);
313 config
314 }
315}
316
317#[derive(Clone)] pub struct BBCode {
321 pub matchers: Arc<Vec<MatchInfo>>, #[cfg(feature = "profiling")]
325 pub profiler: onestop::OneList<onestop::OneDuration>
326}
327
328impl BBCode
329{
330 pub fn default() -> Result<Self, Error> {
332 Ok(Self::from_config(BBCodeTagConfig::default(), None)?)
333 }
334
335 pub fn from_matchers(matchers: Vec<MatchInfo>) -> Self {
338 Self {
339 matchers: Arc::new(matchers),
340 #[cfg(feature = "profiling")]
341 profiler: onestop::OneList::<onestop::OneDuration>::new()
342 }
343 }
344
345 pub fn from_config(config: BBCodeTagConfig, additional_matchers: Option<Vec<MatchInfo>>) -> Result<Self, Error>
351 {
352 let mut matches : Vec<MatchInfo> = Vec::new();
353
354 matches.push(MatchInfo {
356 id: NORMALTEXTID,
357 regex: Regex::new(r#"^[^\[\n\rh]+"#)?,
360 match_type : MatchType::Simple(Arc::new(|c| String::from(html_escape::encode_quoted_attribute(&c[0]))))
361 });
362
363 matches.push(MatchInfo {
365 id: CONSUMETEXTID,
366 regex: Regex::new(r#"^[\r]+"#)?,
367 match_type: MatchType::Simple(Arc::new(|_c| String::new()))
368 });
369
370 let target_attr = match config.link_target {
371 BBCodeLinkTarget::Blank => "target=\"_blank\"",
372 BBCodeLinkTarget::None => ""
373 };
374 let target_attr_c1 = target_attr.clone();
375
376 let mut url_only = Self::plaintext_ids();
377 if config.img_in_url {
378 url_only.push("attr:img")
379 }
380
381 let accepted_tags : Vec<&str> = config.accepted_tags.iter().map(|t| t.as_str()).collect();
382
383 macro_rules! addmatch {
384 ($name:literal, $value:expr) => {
385 addmatch!($name, $value, None, None)
386 };
387 ($name:literal, $value:expr, $open:expr, $close:expr) => {
388 if accepted_tags.contains(&$name) {
389 Self::add_tagmatcher(&mut matches, $name, $value, $open, $close)?
390 }
391 }
392 }
393
394 #[allow(unused_variables)]
395 {
396 addmatch!("b", ScopeInfo::basic(Arc::new(|o,b,c| format!("<b>{b}</b>"))));
398 addmatch!("i", ScopeInfo::basic(Arc::new(|o,b,c| format!("<i>{b}</i>"))));
399 addmatch!("sup", ScopeInfo::basic(Arc::new(|o,b,c| format!("<sup>{b}</sup>"))));
400 addmatch!("sub", ScopeInfo::basic(Arc::new(|o,b,c| format!("<sub>{b}</sub>"))));
401 addmatch!("u", ScopeInfo::basic(Arc::new(|o,b,c| format!("<u>{b}</u>"))));
402 addmatch!("s", ScopeInfo::basic(Arc::new(|o,b,c| format!("<s>{b}</s>"))));
403 addmatch!("list", ScopeInfo::basic(Arc::new(|o,b,c| format!("<ul>{b}</ul>"))), Some((0,1)), Some((0,1)));
404 addmatch!(r"\*", ScopeInfo {
406 only: None, double_closes: true, emit: Arc::new(|o,b,c| format!("<li>{b}</li>"))
407 }, Some((1,0)), Some((1,0)));
408 addmatch!(r"url", ScopeInfo {
409 only: Some(url_only),
410 double_closes: false,
411 emit: Arc::new(move |o,b,c| format!(r#"<a href="{}" {}>{}</a>"#, Self::attr_or_body(&o,b), target_attr, b) )
412 });
413 addmatch!(r"img", ScopeInfo {
414 only: Some(Self::plaintext_ids()),
415 double_closes: false,
416 emit: Arc::new(|o,b,c| format!(r#"<img src="{}">"#, Self::attr_or_body(&o,b)) )
417 });
418
419 addmatch!("h1", ScopeInfo::basic(Arc::new(|_o,b,_c| format!("<h1>{}</h1>",b))), Some((0,1)), Some((1,1)));
421 addmatch!("h2", ScopeInfo::basic(Arc::new(|_o,b,_c| format!("<h2>{}</h2>",b))), Some((0,1)), Some((1,1)));
422 addmatch!("h3", ScopeInfo::basic(Arc::new(|_o,b,_c| format!("<h3>{}</h3>",b))), Some((0,1)), Some((1,1)));
423 addmatch!("anchor", ScopeInfo::basic(
424 Arc::new(|o,b,_c| format!(r##"<a{} href="#{}">{}</a>"##, Self::attr_or_nothing(&o,"name"), Self::attr_or_body(&o,""), b))));
425 addmatch!("quote", ScopeInfo::basic(
426 Arc::new(|o,b,_c| format!(r#"<blockquote{}>{}</blockquote>"#, Self::attr_or_nothing(&o,"cite"), b))), Some((0,1)), Some((0,1)));
427 addmatch!("spoiler", ScopeInfo::basic(
428 Arc::new(|o,b,_c| format!(r#"<details class="spoiler">{}{}</details>"#, Self::tag_or_something(&o,"summary", Some("Spoiler")), b))));
429 addmatch!("icode", ScopeInfo {
430 only: Some(Self::plaintext_ids()),
431 double_closes: false,
432 emit: Arc::new(|_o,b,_c| format!(r#"<span class="icode">{b}</span>"#))
433 });
434 addmatch!("code", ScopeInfo {
435 only: Some(Self::plaintext_ids()),
436 double_closes: false,
437 emit: Arc::new(|o,b,_c| format!(r#"<pre class="code"{}>{}</pre>"#, Self::attr_or_nothing(&o, "data-code"), b) )
438 }, Some((0,1)), Some((0,1)));
439 addmatch!("youtube", ScopeInfo {
440 only: Some(Self::plaintext_ids()),
441 double_closes: false,
442 emit: Arc::new(|o,b,_c| format!(r#"<a href={} target="_blank" data-youtube>{}</a>"#, Self::attr_or_body(&o, b), b) )
443 });
444 }
445
446 if let Some(m) = additional_matchers {
450 matches.extend(m);
451 }
452
453 if config.newline_to_br {
456 matches.push(MatchInfo {
457 id: BRNEWLINEID,
458 regex: Regex::new(r#"^\n"#)?,
459 match_type: MatchType::Simple(Arc::new(|_c| String::from("<br>")))
460 })
461 }
462 matches.push(MatchInfo { id: NEWLINEID,
464 regex: Regex::new(r#"^[\n]+"#)?,
465 match_type: MatchType::Simple(Arc::new(|c| String::from(&c[0])))
466 });
467
468 let url_chars = r#"[-a-zA-Z0-9_/%&=#+~@$*'!?,.;:]*"#;
470 let end_chars = r#"[-a-zA-Z0-9_/%&=#+~@$*']"#;
471 let autolink_regex = format!("^https?://{0}{1}([(]{0}[)]({0}{1})?)?", url_chars, end_chars);
472
473 matches.push(MatchInfo {
475 id: AUTOLINKID,
476 regex: Regex::new(&autolink_regex)?,
479 match_type: MatchType::Simple(Arc::new(move |c| format!(r#"<a href="{0}" {1}>{0}</a>"#, &c[0], target_attr_c1)))
480 });
481
482 Ok(Self::from_matchers(matches))
483 }
484
485 pub fn to_consumer(&mut self)
488 {
489 let new_matchers : Vec<MatchInfo> =
490 self.matchers.iter().map(|m|
491 {
492 match &m.match_type {
493 MatchType::Open(_) | MatchType::Close => {
494 MatchInfo {
495 id: m.id,
496 regex: m.regex.clone(),
497 match_type: MatchType::Simple(Arc::new(|_| String::new()))
498 }
499 },
500 MatchType::Simple(f) => {
501 MatchInfo {
502 id: m.id,
503 regex: m.regex.clone(),
504 match_type: MatchType::Simple(f.clone())
505 }
506 }
507 }
508 }).collect();
509 self.matchers = Arc::new(new_matchers);
510 }
511
512 pub fn get_tagregex(tag: &'static str, open_consume: Option<(i32,i32)>, close_consume: Option<(i32,i32)>) -> (String, String)
514 {
515 let pre_openchomp; let post_openchomp; let pre_closechomp; let post_closechomp;
516 match open_consume {
517 Some((pre, post)) => {
518 pre_openchomp = format!("(?:\r?\n){{0,{}}}", pre);
519 post_openchomp = format!("(?:\r?\n){{0,{}}}", post);
520 },
521 None => {
522 pre_openchomp = String::new();
523 post_openchomp = String::new();
524 }
525 }
526 match close_consume {
527 Some((pre, post)) => {
528 pre_closechomp = format!("(?:\r?\n){{0,{}}}", pre);
529 post_closechomp = format!("(?:\r?\n){{0,{}}}", post);
530 },
531 None => {
532 pre_closechomp = String::new();
533 post_closechomp = String::new();
534 }
535 }
536 let open_tag = format!(r#"^{0}\[{1}((?:[ \t]+{1})?=(?P<attr>[^\]\n]*))?\]{2}"#, pre_openchomp, Self::tag_insensitive(tag), post_openchomp);
537 let close_tag = format!(r#"^{}\[/{}\]{}"#, pre_closechomp, Self::tag_insensitive(tag), post_closechomp);
538 (open_tag, close_tag)
539 }
540
541 pub fn add_tagmatcher(matchers: &mut Vec<MatchInfo>, tag: &'static str, info: ScopeInfo, open_consume: Option<(i32,i32)>, close_consume: Option<(i32,i32)>) -> Result<(), Error> { let (open_tag, close_tag) = Self::get_tagregex(tag, open_consume, close_consume);
545 matchers.push(MatchInfo {
546 id: tag,
547 regex: Regex::new(&open_tag)?,
548 match_type: MatchType::Open(Arc::new(info))
549 });
550 matchers.push(MatchInfo {
551 id: tag,
552 regex: Regex::new(&close_tag)?,
553 match_type: MatchType::Close,
554 });
555 Ok(())
556 }
557
558 fn tag_insensitive(tag: &str) -> String {
560 let mut result = String::with_capacity(tag.len() * 4);
561 let mut skip = 0;
562 for c in tag.to_ascii_lowercase().chars() {
563 if c == '\\' {
564 skip = 2;
565 }
566 if skip > 0 {
567 skip -= 1;
568 result.push(c);
569 continue;
570 }
571 result.push_str("[");
572 result.push(c);
573 result.push(c.to_ascii_uppercase());
574 result.push_str("]");
575 }
576 result
577 }
578
579 fn attr_or_body(opener: &Option<Captures>, body: &str) -> String {
580 if let Some(opener) = opener {
581 if let Some(group) = opener.name("attr") {
582 return String::from(html_escape::encode_quoted_attribute(group.as_str()));
583 }
584 }
585 return String::from(body);
586 }
587
588 fn attr_or_nothing(opener: &Option<Captures>, name: &str) -> String {
589 if let Some(opener) = opener {
590 if let Some(group) = opener.name("attr") {
591 return format!(" {}=\"{}\"", name, html_escape::encode_quoted_attribute(group.as_str()));
593 }
594 }
595 return String::new();
596 }
597
598 fn tag_or_something(opener: &Option<Captures>, tag: &str, something: Option<&str>) -> String {
599 if let Some(opener) = opener {
600 if let Some(group) = opener.name("attr") {
601 return format!("<{0}>{1}</{0}>", tag, html_escape::encode_quoted_attribute(group.as_str()));
603 }
604 }
605 if let Some(something) = something {
606 return format!("<{0}>{1}</{0}>", tag, html_escape::encode_quoted_attribute(something));
607 }
608 return String::new();
609 }
610
611 pub fn plaintext_ids() -> Vec<&'static str> {
612 vec![NORMALTEXTID, CONSUMETEXTID]
613 }
614
615 pub fn parse(&self, input: &str) -> String
619 {
620 let mut slice = &input[0..]; let mut scoper = BBScoper::new();
625
626 while slice.len() > 0
629 {
630 let mut matched_info : Option<&MatchInfo> = None;
631
632 for matchinfo in self.matchers.iter() {
636 if !scoper.is_allowed(matchinfo) {
637 continue;
638 }
639 else if matchinfo.regex.is_match(slice) {
640 matched_info = Some(matchinfo);
641 break;
642 }
643 }
644
645 if let Some(tagdo) = matched_info
647 {
648 for captures in tagdo.regex.captures_iter(slice) {
650 slice = &slice[captures[0].len()..];
651 match &tagdo.match_type {
652 MatchType::Simple(closure) => {
653 scoper.add_text(&closure(captures));
654 }
655 MatchType::Open(info) => {
656 let new_scope = BBScope {
658 id: tagdo.id,
659 info: info.clone(),
660 open_tag_capture: Some(captures),
661 body: String::new()
662 };
663 scoper.add_scope(new_scope);
664 },
665 MatchType::Close => {
666 scoper.close_scope(tagdo.id);
669 }
670 }
671 }
672 }
673 else {
675 if let Some(ch) = slice.chars().next() {
679 scoper.add_char(ch);
680 slice = &slice[ch.len_utf8()..];
681 }
682 else {
683 println!("In BBCode::parse, there were no more characters but there were leftover bytes!");
684 break;
685 }
686 }
687 }
688
689 scoper.dump_remaining()
690 }
691
692 pub fn parse_profiled_opt(&mut self, input: &str, _name: String) -> String
694 {
695 #[cfg(feature = "profiling")]
696 {
697 let mut profile = onestop::OneDuration::new(_name);
698 let result = self.parse(input);
699 profile.finish();
700 self.profiler.add(profile);
701 result
702 }
703
704 #[cfg(not(feature = "profiling"))]
705 return self.parse(input);
706 }
707
708}
709
710
711#[cfg(test)]
716mod tests {
717 use super::*;
718
719 macro_rules! bbtest_basics {
720 ($($name:ident: $value:expr;)*) => {
721 $(
722 #[test]
723 fn $name() {
724 let bbcode = BBCode::default().unwrap(); let (input, expected) = $value;
726 assert_eq!(bbcode.parse(input), expected);
727 }
728 )*
729 }
730 }
731
732 macro_rules! bbtest_nondefaults{
733 ($($name:ident: $value:expr;)*) => {
734 $(
735 #[test]
736 fn $name() {
737 let mut config = BBCodeTagConfig::default();
738 config.link_target = BBCodeLinkTarget::None;
739 config.img_in_url = false;
740 config.newline_to_br = false;
741 config.accepted_tags = vec![String::from("b"), String::from("u"), String::from("code"), String::from("url"), String::from("img")];
742 let bbcode = BBCode::from_config(config, None).unwrap();
743 let (input, expected) = $value;
744 assert_eq!(bbcode.parse(input), expected);
745 }
746 )*
747 }
748 }
749
750 macro_rules! bbtest_extras {
751 ($($name:ident: $value:expr;)*) => {
752 $(
753 #[test]
754 fn $name() {
755 let mut matchers = Vec::<MatchInfo>::new(); let color_emitter : EmitScope = Arc::new(|open_capture,body,_c| {
757 let color = open_capture.unwrap().name("attr").unwrap().as_str();
758 format!(r#"<span style="color:{}">{}</span>"#, color, body)
759 });
760 BBCode::add_tagmatcher(&mut matchers, "color", ScopeInfo::basic(color_emitter), None, None).unwrap();
761 let bbcode = BBCode::from_config(BBCodeTagConfig::extended(), Some(matchers)).unwrap();
762 let (input, expected) = $value;
763 assert_eq!(bbcode.parse(input), expected);
764 }
765 )*
766 }
767 }
768
769 macro_rules! bbtest_consumer {
770 ($($name:ident: $value:expr;)*) => {
771 $(
772 #[test]
773 fn $name() {
774 let mut bbcode = BBCode::from_config(BBCodeTagConfig::extended(), None).unwrap();
775 bbcode.to_consumer();
776 let (input, expected) = $value;
777 assert_eq!(bbcode.parse(input), expected);
778 }
779 )*
780 }
781 }
782
783 #[test]
784 fn build_init() {
785 let _bbcode = BBCode::default().unwrap();
787 }
788
789 #[cfg(feature = "bigtest")]
791 #[test]
792 fn performance_issues()
793 {
794 use pretty_assertions::{assert_eq};
795
796 let bbcode = BBCode::from_config(BBCodeTagConfig::extended(), None).unwrap();
797
798 let testdir = "bigtests";
799 let entries = std::fs::read_dir(testdir).unwrap();
800 let mut checks: Vec<(String,String,String)> = Vec::new();
801 for entry in entries
802 {
803 let entry = entry.unwrap();
804 let path = entry.path();
805 let metadata = std::fs::metadata(&path).unwrap();
806
807 if metadata.is_file() {
809 let base_text = std::fs::read_to_string(&path).unwrap();
810 let parse_path = std::path::Path::new(testdir).join("parsed").join(path.file_name().unwrap());
811 let parse_text = std::fs::read_to_string(&parse_path).unwrap();
812 checks.push((base_text, parse_text, String::from(path.file_name().unwrap().to_str().unwrap())));
813 println!("Found test file: {:?}", path);
814 }
815 }
816 println!("Total tests: {}", checks.len());
817 let start = std::time::Instant::now();
818 for (raw, parsed, path) in checks {
819 let test_start = start.elapsed();
820 let result = bbcode.parse(&raw);
821 let test_end = start.elapsed();
822 assert_eq!(result, parsed);
823 println!(" Test '{}' : {:?}", path, test_end - test_start);
824 }
825 let elapsed = start.elapsed();
826 println!("Parse total: {:?}", elapsed);
827 }
828
829 #[cfg(feature = "bigtest")]
830 #[test] fn benchmark_10000() {
832 let bbcode = BBCode::from_config(BBCodeTagConfig::extended(), None).unwrap();
833 let parselem = vec![
834 ("it's a %CRAZY% <world> ๐=\"yeah\" ๐จโ๐จโ๐งโ๐ฆ>>done",
835 "it's a %CRAZY% <world> ๐="yeah" ๐จโ๐จโ๐งโ๐ฆ>>done"),
836 ("[][[][6][a[ab]c[i]italic[but][][* not] 8[]]][", "[][[][6][a[ab]c<i>italic[but][][* not] 8[]]][</i>"),
837 ("[url]this[b]is[/b]a no-no[i][/url]", r#"<a href="this[b]is[/b]a no-no[i]" target="_blank">this[b]is[/b]a no-no[i]</a>"#),
838 ("[img=https://old.smiflebosicswoace.com/user_uploads/avatars/t1647374379.png]abc 123[/img]", r#"<img src="https://old.smiflebosicswoace.com/user_uploads/avatars/t1647374379.png">"#),
839 ("[spoiler]this[b]is empty[/spoiler]", r#"<details class="spoiler"><summary>Spoiler</summary>this<b>is empty</b></details>"#)
840 ];
841
842 let start = std::time::Instant::now();
843 for i in 0..10000 {
844 if let Some((input, output)) = parselem.get(i % parselem.len()) {
845 if bbcode.parse(*input) != *output {
846 panic!("Hang on, bbcode isn't working!");
847 }
848 }
849 else {
850 panic!("WHAT? INDEX OUT OF BOUNDS??");
851 }
852 }
853 let elapsed = start.elapsed();
854 println!("10000 iterations took: {:?}", elapsed);
855 }
856
857 bbtest_basics! {
858 no_alter: ("hello", "hello");
859 lt_single: ("h<ello", "h<ello");
860 gt_single: ("h>ello", "h>ello");
861 amp_single: ("h&ello", "h&ello");
862 quote_single: ("h'ello", "h'ello");
863 doublequote_single: ("h\"ello", "h"ello");
864 return_byebye: ("h\rello", "hello");
865 newline_br: ("h\nello", "h<br>ello");
866 complex_escape: (
867 "it's a %CRAZY% <world> ๐=\"yeah\" ๐จโ๐จโ๐งโ๐ฆ>>done",
868 "it's a %CRAZY% <world> ๐="yeah" ๐จโ๐จโ๐งโ๐ฆ>>done"
869 );
870 simple_bold: ("[b]hello[/b]", "<b>hello</b>");
872 simple_sup: ("[sup]hello[/sup]", "<sup>hello</sup>");
873 simple_sub: ("[sub]hello[/sub]", "<sub>hello</sub>");
874 simple_strikethrough: ("[s]hello[/s]", "<s>hello</s>");
875 simple_underline: ("[u]hello[/u]", "<u>hello</u>");
876 simple_italic: ("[i]hello[/i]", "<i>hello</i>");
877 simple_nospaces: ("[b ]hello[/ b]", "[b ]hello[/ b]");
878 simple_insensitive: ("[sUp]hello[/SuP]", "<sup>hello</sup>");
880 simple_sensitivevalue: ("[sUp]OK but The CAPITALS[/SuP]YEA", "<sup>OK but The CAPITALS</sup>YEA");
881 simple_bolditalic: ("[b][i]hello[/i][/b]", "<b><i>hello</i></b>");
882 nested_bold: ("[b]hey[b]extra bold[/b] less bold again[/b]", "<b>hey<b>extra bold</b> less bold again</b>");
883 simple_url_default: ("[url]https://google.com[/url]", r#"<a href="https://google.com" target="_blank">https://google.com</a>"#);
884 simple_url_witharg: ("[url=http://ha4l6o7op9dy.com]furries lol[/url]", r#"<a href="http://ha4l6o7op9dy.com" target="_blank">furries lol</a>"#);
885 url_escape: ("[url=http'://ha4l<6o7op9dy>.com]furries lol[/url]", r#"<a href="http'://ha4l<6o7op9dy>.com" target="_blank">furries lol</a>"#);
886 simple_img: ("[img]https://old.smiflebosicswoace.com/user_uploads/avatars/t1647374379.png[/img]", r#"<img src="https://old.smiflebosicswoace.com/user_uploads/avatars/t1647374379.png">"#);
887 simple_img_nonstd: ("[img=https://old.smiflebosicswoace.com/user_uploads/avatars/t1647374379.png][/img]", r#"<img src="https://old.smiflebosicswoace.com/user_uploads/avatars/t1647374379.png">"#);
888 simple_img_nonstd_inner: ("[img=https://old.smiflebosicswoace.com/user_uploads/avatars/t1647374379.png]abc 123[/img]", r#"<img src="https://old.smiflebosicswoace.com/user_uploads/avatars/t1647374379.png">"#);
891 url_with_img: ("[url=https://google.com][img]https://some.image.url/junk.png[/img][/url]", r#"<a href="https://google.com" target="_blank"><img src="https://some.image.url/junk.png"></a>"#);
893 url_with_img_attr: ("[url=https://google.com][img=https://some.image.url/junk.png][/url]", r#"<a href="https://google.com" target="_blank"><img src="https://some.image.url/junk.png"></a>"#);
894 url_with_img_nourl: ("[url][img=https://some.image.url/junk.png][/url]", r#"<a href="[img=https://some.image.url/junk.png]" target="_blank">[img=https://some.image.url/junk.png]</a>"#);
897 url_no_other_tags: ("[url=https://what.non][b][i][u][s][/url]", r#"<a href="https://what.non" target="_blank">[b][i][u][s]</a>"#);
898 url_nested: ("[url=https://what.non][url=https://abc123.com][/url][/url]", r#"<a href="https://what.non" target="_blank">[url=https://abc123.com]</a>"#);
902 list_basic: ("[list][*]item 1[/*][*]item 2[/*][*]list[/*][/list]", "<ul><li>item 1</li><li>item 2</li><li>list</li></ul>");
904 unclosed_basic: ("[b] this is bold [i]also italic[/b] oops close all[/i]", "<b> this is bold <i>also italic</i></b> oops close all");
905 verbatim_url: ("[url]this[b]is[/b]a no-no[i][/url]", r#"<a href="this[b]is[/b]a no-no[i]" target="_blank">this[b]is[/b]a no-no[i]</a>"#);
906 inner_hack: ("[[b][/b]b]love[/[b][/b]b]", "[<b></b>b]love[/<b></b>b]");
907 random_brackets: ("[][[][6][a[ab]c[i]italic[but][][* not] 8[]]][", "[][[][6][a[ab]c<i>italic[but][][* not] 8[]]][</i>");
908 autolink_basic: ("this is https://google.com ok?", r#"this is <a href="https://google.com" target="_blank">https://google.com</a> ok?"#);
909
910 newline_list1: ("[list]\n[*]item", "<ul><li>item</li></ul>");
911 newline_list2: ("[list]\r\n[*]item", "<ul><li>item</li></ul>");
912 newline_listmega: ("\n[list]\r\n[*]item\r\n[*]item2 yeah[\r\n\r\n[*]three", "<br><ul><li>item</li><li>item2 yeah[<br></li><li>three</li></ul>");
913 newline_bold: ("\n[b]\nhellow\n[/b]\n", "<br><b><br>hellow<br></b><br>");
915 newline_italic: ("\n[i]\nhellow\n[/i]\n", "<br><i><br>hellow<br></i><br>");
916 newline_underline: ("\n[u]\nhellow\n[/u]\n", "<br><u><br>hellow<br></u><br>");
917 newline_strikethrough: ("\n[s]\nhellow\n[/s]\n", "<br><s><br>hellow<br></s><br>");
918 newline_sup: ("\n[sup]\nhellow\n[/sup]\n", "<br><sup><br>hellow<br></sup><br>");
919 newline_sub: ("\n[sub]\nhellow\n[/sub]\n", "<br><sub><br>hellow<br></sub><br>");
920 consume_attribute: ("[b=haha ok]but maybe? [/b]{no}", "<b>but maybe? </b>{no}");
921
922 e_dangling: ("[b]foo", "<b>foo</b>");
924 e_normal: ("[b]foo[/b]", "<b>foo</b>");
925 e_nested: ("[b]foo[b]bar[/b][/b]", "<b>foo<b>bar</b></b>");
926 e_empty: ("[b]foo[b][/b]bar[/b]", "<b>foo<b></b>bar</b>");
927 e_closemulti: ("[b]foo[i]bar[u]baz[/b]quux", "<b>foo<i>bar<u>baz</u></i></b>quux");
928 e_faketag: ("[b]foo[i]bar[u]baz[/fake]quux", "<b>foo<i>bar<u>baz[/fake]quux</u></i></b>");
929 e_reallyfake: ("[fake][b]foo[i]bar[u]baz[/fake]quux", "[fake]<b>foo<i>bar<u>baz[/fake]quux</u></i></b>");
930 e_ignoreclose: ("[b]foo[/b]bar[/b][/b][/b]", "<b>foo</b>bar");
931 e_weirdignoreclose: ("[b]foo[/b]bar[/fake][/b][/fake]", "<b>foo</b>bar[/fake][/fake]");
932 e_fancytag: ("[[i]b[/i]]", "[<i>b</i>]");
933 e_escapemadness: ("&[&]<[<]>[>]", "&[&]<[<]>[>]");
934 e_bracket_url: ("[url=#Ports][1][/url]", r##"<a href="#Ports" target="_blank">[1]</a>"##);
935 }
936
937 bbtest_nondefaults! {
938 restricted_tags: ("[s]not supported haha![/s]", "[s]not supported haha![/s]");
939 newlines_not_br: ("[b]this\n[i]\nis\n[u]silly!\n[/s]\n", "<b>this\n[i]\nis\n<u>silly!\n[/s]\n</u></b>");
940 no_target_in_url: ("[url=https://valid.com]target[/url]", "<a href=\"https://valid.com\" >target</a>");
941 no_img_in_url: ("[url=https://valid.com][img=https://notvalid.net][/url]", "<a href=\"https://valid.com\" >[img=https://notvalid.net]</a>");
943 no_img_in_url_noendtag: ("[url=https://valid.com][img=https://notvalid.net][/img]", "<a href=\"https://valid.com\" >[img=https://notvalid.net][/img]</a>");
944 }
945
946 bbtest_extras! {
947 e_emptyquote: ("[quote]...[/quote]", "<blockquote>...</blockquote>");
948 e_normalquote: ("[quote=foo]...[/quote]", r#"<blockquote cite="foo">...</blockquote>"#);
949 simple_spoiler: ("[spoiler=wow]amazing[/spoiler]", r#"<details class="spoiler"><summary>wow</summary>amazing</details>"#);
950 simple_emptyspoiler: ("[spoiler]this[b]is empty[/spoiler]", r#"<details class="spoiler"><summary>Spoiler</summary>this<b>is empty</b></details>"#);
951 spoiler_simeon: ("[spoiler spoiler=what is this]i hate it[/spoiler]", r#"<details class="spoiler"><summary>what is this</summary>i hate it</details>"#);
952 cite_escape: ("[quote=it's<mad>lad]yeah[/quote]",r#"<blockquote cite="it's<mad>lad">yeah</blockquote>"#);
953 h1_simple: ("[h1] so about that header [/h1]", "<h1> so about that header </h1>");
954 h2_simple: (" [h2]Not as important", " <h2>Not as important</h2>");
955 h3_simple: ("[h3][h3]wHaAt-Are-u-doin[/h3]", "<h3><h3>wHaAt-Are-u-doin</h3></h3>");
956 quote_newlines: ("\n[quote]\n\nthere once was\na boy\n[/quote]\n", "<br><blockquote><br>there once was<br>a boy<br></blockquote>");
957 anchor_simple: ("[anchor=Look_Here]The Title[/anchor]", r##"<a name="Look_Here" href="#Look_Here">The Title</a>"##);
958 anchor_inside: ("[anchor=name][h1]A title[/h1][/anchor]", r##"<a name="name" href="#name"><h1>A title</h1></a>"##);
959 icode_simple: ("[icode=Nothing Yet]Some[b]code[url][/i][/icode]", r#"<span class="icode">Some[b]code[url][/i]</span>"#);
960 code_simple: ("\n[code=SB3]\nSome[b]code[url][/i]\n[/code]\n", "<br><pre class=\"code\" data-code=\"SB3\">Some[b]code[url][/i]\n</pre>");
961 simple_customtag: ("[color=wow]amazing[/color]", r#"<span style="color:wow">amazing</span>"#);
962 simple_customtag_withdefault: ("[color=#FF5500][b][i]ama\nzing[/color]", r#"<span style="color:#FF5500"><b><i>ama<br>zing</i></b></span>"#);
963 }
964
965 bbtest_consumer! {
966 consume_standard: ("[b]wow[/b] but like [i]uh no scoping [s] rules [/sup] and ugh[/quote]", "wow but like uh no scoping rules and ugh");
967 consume_stillescape: ("<>'\"oof[img=wow][url][code][/url][/code]\n", "<>'"oof");
969 }
970}