#[cfg(any(feature = "track_open_tags", feature = "parser_rules"))]
use alloc::vec;
use alloc::{boxed::Box, vec::Vec};
use core::marker::PhantomData;
use static_assertions::{assert_impl_all, assert_not_impl_all};
use bitflags::bitflags;
#[derive(Default)]
pub struct ParserConfig {
pub feature_flags: ParserFeature,
}
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ParserFeature: u32 {
const POP_UNORDERED = 1 << 1;
const UNMATCHED_CLOSE_AS_TEXT = 1 << 2;
const V1 = Self::POP_UNORDERED.bits() | Self::UNMATCHED_CLOSE_AS_TEXT.bits();
const ALL = u32::MAX;
}
}
#[cfg_attr(
not(any(feature = "track_open_tags", feature = "parser_rules")),
doc = "BBParser does not allocate on the current configuration."
)]
#[cfg_attr(
any(feature = "track_open_tags", feature = "parser_rules"),
doc = "BBParser allocates on the current configuration when:"
)]
#[cfg_attr(
feature = "track_open_tags",
doc = "- An opening tag is encountered. (`track_open_tags`)"
)]
#[cfg_attr(
feature = "parser_rules",
doc = "- A parser rule is inserted. (`parser_rules`)"
)]
#[doc(alias = "parser")]
pub struct BBParser<'a, CustomTy = ()>
where
CustomTy: Clone,
{
input: &'a str,
config: ParserConfig,
loc: usize,
#[cfg(feature = "track_open_tags")]
open_tags: Vec<Token<'a, CustomTy>>,
#[cfg(feature = "track_open_tags")]
closed_tags: Vec<Token<'a, CustomTy>>,
#[cfg(feature = "parser_rules")]
rule_stack: Vec<Box<dyn rules::ParserRuleObj<'a, CustomTy> + Send + 'a>>,
_custom_ty: PhantomData<CustomTy>,
}
assert_impl_all!(BBParser<'_, ()>: Send);
assert_not_impl_all!(BBParser<'_, *const u8>: Send);
impl<'a> BBParser<'a> {
pub fn new(input: &'a str) -> BBParser<'a> {
Self::new_with_custom(input)
}
pub fn with_config(input: &'a str, config: ParserConfig) -> BBParser<'a> {
Self::with_config_and_custom(input, config)
}
}
impl<'a> BBParser<'a> {
pub fn new_with_custom<CustomTy>(input: &'a str) -> BBParser<'a, CustomTy>
where
CustomTy: Clone,
{
BBParser::<'a, CustomTy> {
input,
config: Default::default(),
loc: 0,
#[cfg(feature = "track_open_tags")]
open_tags: vec![],
#[cfg(feature = "track_open_tags")]
closed_tags: vec![],
#[cfg(feature = "parser_rules")]
rule_stack: vec![],
_custom_ty: PhantomData,
}
}
pub fn with_config_and_custom<CustomTy>(
input: &'a str,
config: ParserConfig,
) -> BBParser<'a, CustomTy>
where
CustomTy: Clone,
{
BBParser::<'a, CustomTy> {
input,
config,
loc: 0,
#[cfg(feature = "track_open_tags")]
open_tags: vec![],
#[cfg(feature = "track_open_tags")]
closed_tags: vec![],
#[cfg(feature = "parser_rules")]
rule_stack: vec![],
_custom_ty: PhantomData,
}
}
}
impl<'a, CustomTy> BBParser<'a, CustomTy>
where
CustomTy: Clone,
{
pub fn remaining(&self) -> &str {
&self.input[self.loc..]
}
pub fn remaining_after(&self, after: usize) -> &str {
&self.input[(self.loc + after)..]
}
#[cfg(feature = "track_open_tags")]
pub fn open_tags(&self) -> &[Token<'a, CustomTy>] {
&self.open_tags
}
#[cfg(feature = "track_open_tags")]
pub fn closed_tags(&self) -> &[Token<'a, CustomTy>] {
&self.closed_tags
}
}
impl<'a, CustomTy> Iterator for BBParser<'a, CustomTy>
where
CustomTy: Clone,
{
type Item = Token<'a, CustomTy>;
fn next(&mut self) -> Option<Self::Item> {
fn to_token_kind<'a, CustomTy>(tag: &'a str, args: &'a str) -> TokenKind<'a, CustomTy> {
if tag.starts_with('/') {
TokenKind::CloseBBTag(
BBTag {
tag: &tag["/".len()..],
args,
},
None,
)
} else if args.ends_with('/') {
TokenKind::StandaloneBBTag(BBTag {
tag,
args: &args[..(args.len() - "/".len())],
})
} else {
TokenKind::OpenBBTag(BBTag { tag, args })
}
}
fn to_token_kind_single<CustomTy>(tag: &str) -> TokenKind<'_, CustomTy> {
if tag.starts_with('/') {
TokenKind::CloseBBTag(
BBTag {
tag: &tag["/".len()..],
args: "",
},
None,
)
} else if tag.ends_with('/') {
TokenKind::StandaloneBBTag(BBTag {
tag: &tag[..(tag.len() - "/".len())],
args: "",
})
} else {
TokenKind::OpenBBTag(BBTag { tag, args: "" })
}
}
const TAG_OPENERS: &[char] = &['['];
if self.loc >= self.input.len() {
return None;
}
let first_char = self.remaining().chars().nth(0)?;
let mut token = 'tk: {
#[cfg(feature = "parser_rules")]
{
let action = self.rule_stack.last().map(|x| x.action());
if let Some(rules::ParserRuleAction::CustomParser) = action {
let token = self.rule_stack.last_mut().unwrap().parse_custom(self.input);
self.loc += token.span.len();
break 'tk token;
}
}
'no_match: {
if TAG_OPENERS.contains(&first_char) {
let loc = first_char.len_utf8();
let rem_after = { &self.input[(self.loc + loc)..] };
let tag_end = rem_after.find(']');
if tag_end.is_none() {
break 'no_match;
}
let tag_end = tag_end.unwrap();
let tag_contents = rem_after[..tag_end].trim();
if tag_contents.matches(TAG_OPENERS).count() > 0 {
break 'no_match;
}
let span = &self.input[self.loc..(self.loc + tag_end + "[]".len())];
let old_loc = self.loc;
self.loc += span.len();
if let Some(arg_idx) = tag_contents.find(['=', ' ']) {
let (tag, args) = tag_contents.split_at(arg_idx);
break 'tk Token::<'a, CustomTy> {
span,
start: old_loc,
kind: to_token_kind(tag, args),
};
} else {
break 'tk Token::<'a, CustomTy> {
span,
start: old_loc,
kind: to_token_kind_single(tag_contents),
};
}
}
}
let segment_end = if !TAG_OPENERS.contains(&first_char) {
self.remaining()
.match_indices(TAG_OPENERS)
.nth(0)
.map(|x| x.0)
.unwrap_or(self.remaining().len())
} else {
self.remaining_after("[".len())
.match_indices(TAG_OPENERS)
.nth(0)
.map(|x| x.0 + "[".len())
.unwrap_or(self.remaining().len())
};
let range = self.loc..(self.loc + segment_end);
self.loc += range.len();
break 'tk Token::<'a, CustomTy> {
start: range.start,
span: &self.input[range],
kind: TokenKind::Text,
};
};
#[cfg(feature = "parser_rules")]
{
let do_pop = if let Some(rule) = self.rule_stack.last() {
rule.transform_token(&mut token)
} else {
false
};
if do_pop {
self.rule_stack.pop();
}
let action = self.rule_stack.last().map(|x| x.action());
if let Some(action) = action {
match action {
rules::ParserRuleAction::NoParse => {
token.rewrite_as_text();
}
rules::ParserRuleAction::CustomParser => {}
}
}
}
#[cfg(feature = "track_open_tags")]
{
if let TokenKind::OpenBBTag(_) = token.kind {
self.open_tags.push(token.clone());
}
if let TokenKind::CloseBBTag(BBTag { tag: removee, .. }, _) = token.kind {
let to_remove: Option<usize> = 'blk: {
for (idx, tag) in self.open_tags.iter().enumerate().rev() {
if let TokenKind::OpenBBTag(ref t) = tag.kind {
if t.tag.eq_ignore_ascii_case(removee) {
break 'blk Some(idx);
} else if !self
.config
.feature_flags
.contains(ParserFeature::POP_UNORDERED)
{
break 'blk None;
}
} else {
unreachable!(
"Tag stack should never contain anything except open tags."
);
}
}
None
};
if let Some(to_remove) = to_remove {
let tk = self.open_tags.remove(to_remove);
self.closed_tags.push(tk);
token.rewrite_with_opening_tag(self.closed_tags.len() - 1);
} else if self
.config
.feature_flags
.contains(ParserFeature::UNMATCHED_CLOSE_AS_TEXT)
{
token.rewrite_as_text();
}
}
}
Some(token)
}
}
#[derive(Clone)]
pub struct Token<'a, CustomTy>
where
CustomTy: Clone,
{
pub span: &'a str,
pub start: usize,
pub kind: TokenKind<'a, CustomTy>,
}
impl<'a, CustomTy> Token<'a, CustomTy>
where
CustomTy: Clone,
{
pub fn args(&self) -> Option<&str> {
let args = match self.kind {
TokenKind::OpenBBTag(BBTag { args, .. }) => Some(args),
TokenKind::CloseBBTag(BBTag { args, .. }, _) => Some(args),
TokenKind::StandaloneBBTag(BBTag { args, .. }) => Some(args),
_ => None,
};
match args {
Some(args) if !args.trim().is_empty() => Some(args.trim()),
_ => None,
}
}
pub fn tag_name(&self) -> Option<&str> {
match self.kind {
TokenKind::OpenBBTag(BBTag { tag, .. }) => Some(tag),
TokenKind::CloseBBTag(BBTag { tag, .. }, _) => Some(tag),
TokenKind::StandaloneBBTag(BBTag { tag, .. }) => Some(tag),
_ => None,
}
}
pub fn is_open(&self, tag_name: &str) -> bool {
if let TokenKind::OpenBBTag(BBTag { tag, .. }) = self.kind {
tag.eq_ignore_ascii_case(tag_name)
} else {
false
}
}
pub fn is_open_argless(&self, tag_name: &str) -> bool {
self.is_open(tag_name) && self.args().is_none()
}
pub fn is_close(&self, tag_name: &str) -> bool {
if let TokenKind::CloseBBTag(BBTag { tag, .. }, ..) = self.kind {
tag.eq_ignore_ascii_case(tag_name)
} else {
false
}
}
pub fn is_close_argless(&self, tag_name: &str) -> bool {
self.is_close(tag_name) && self.args().is_none()
}
pub fn is_standalone(&self, tag_name: &str) -> bool {
if let TokenKind::StandaloneBBTag(BBTag { tag, .. }) = self.kind {
tag.eq_ignore_ascii_case(tag_name)
} else {
false
}
}
pub fn is_standalone_argless(&self, tag_name: &str) -> bool {
self.is_standalone(tag_name) && self.args().is_none()
}
pub fn is_text(&self) -> bool {
matches!(self.kind, TokenKind::Text)
}
}
impl<'a, CustomTy> Token<'a, CustomTy>
where
CustomTy: Clone,
{
pub fn rewrite_as_text(&mut self) {
self.kind = TokenKind::Text;
}
pub fn rewrite_with_opening_tag(&mut self, idx: usize) {
let TokenKind::CloseBBTag(ref t, _) = self.kind else {
unimplemented!("Can't set the opening tag index on anything except a closing tag!")
};
self.kind = TokenKind::CloseBBTag(t.clone(), Some(idx));
}
}
impl<'a, CustomTy: core::fmt::Debug> core::fmt::Debug for Token<'a, CustomTy>
where
CustomTy: Clone,
{
#[cfg_attr(coverage_nightly, coverage(off))]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Token")
.field("span", &self.span)
.field("start", &self.start)
.field("kind", &self.kind)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct BBTag<'a> {
pub tag: &'a str,
pub args: &'a str,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum TokenKind<'a, CustomTy = ()> {
OpenBBTag(BBTag<'a>),
CloseBBTag(BBTag<'a>, Option<usize>),
StandaloneBBTag(BBTag<'a>),
Text,
Custom(CustomTy),
}
#[cfg(feature = "parser_rules")]
pub mod rules;
#[cfg(test)]
mod tests;