#[cfg(test)]
mod test;
use std::{
borrow::Cow,
fmt::{Debug, Display},
};
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
use chumsky::{
combinator::Repeated,
error::EmptyErr,
extra,
input::InputRef,
primitive::{any, choice, custom, empty, end, just, one_of, OneOf},
recursive::recursive,
text::{int, newline},
IterParser, Parser,
};
use either::Either::{self, Left, Right};
use itertools::Itertools;
fn just_case_insensitive<'src>(
s: &'static str,
) -> impl Parser<'src, &'src str, (), extra::Default> + Copy {
any()
.repeated()
.exactly(s.chars().count())
.to_slice()
.filter(move |g: &&str| g.to_lowercase() == s)
.ignored()
}
use crate::{
ast::CurlyKind, ContextWorker, ContextualizationError, Contextualizer, Escaper, Value,
};
#[derive(Debug, Clone, PartialEq, Eq)]
enum ContextualizerState {
Html(HtmlState),
Css(CssState),
Js(JsState),
PlainText,
Unsafe,
}
impl Display for ContextualizerState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContextualizerState::Html(state) => write!(f, "html{state}"),
ContextualizerState::Css(state) => write!(f, "css{state}"),
ContextualizerState::Js(state) => write!(f, "js{state}"),
ContextualizerState::PlainText => f.write_str("text"),
ContextualizerState::Unsafe => f.write_str("unsafe"),
}
}
}
impl ContextualizerState {
fn name_parser<'src>() -> impl Parser<'src, &'src str, ContextualizerState> + Clone {
recursive(|root_name_parser| {
choice((
just("html")
.ignore_then(HtmlState::name_parser(root_name_parser.clone()))
.map(ContextualizerState::Html),
just("css")
.ignore_then(CssState::name_parser())
.map(ContextualizerState::Css),
just("js")
.ignore_then(JsState::name_parser())
.map(ContextualizerState::Js),
just("text").to(ContextualizerState::PlainText),
just("unsafe").to(ContextualizerState::Unsafe),
))
})
}
}
impl TryFrom<&str> for ContextualizerState {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
ContextualizerState::name_parser()
.parse(value)
.into_result()
.map_err(|_| ())
}
}
impl ContextualizerState {
fn parser<'src>(&self) -> impl Parser<'src, &'src str, ContextualizerState> {
match self {
Self::Html(state) => state.parser().map(ContextualizerState::Html).boxed(),
Self::Css(state) => state.parser().map(ContextualizerState::Css).boxed(),
Self::Js(state) => state.parser().map(ContextualizerState::Js).boxed(),
Self::PlainText => any().repeated().to(ContextualizerState::PlainText).boxed(),
Self::Unsafe => any().repeated().to(ContextualizerState::Unsafe).boxed(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CssState {
Regular,
String(DelimiterKind),
MultiLineComment,
LineComment,
}
impl HasEmptyStategy for CssState {
fn empty_strategy(&self) -> EmptyStrategy {
EmptyStrategy::None
}
}
impl HasMultiLineStrategy for CssState {
fn multi_line_strategy(&self) -> MultiLineStrategy {
match self {
Self::LineComment | Self::String(_) => MultiLineStrategy::Condense,
_ => MultiLineStrategy::None,
}
}
}
impl Display for CssState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Regular => Ok(()),
Self::String(delim) => write!(f, ".string.{delim}"),
Self::MultiLineComment => write!(f, ".multiline_comment"),
Self::LineComment => write!(f, ".line_comment"),
}
}
}
impl NameParser for CssState {
fn name_parser<'src>() -> impl Parser<'src, &'src str, CssState> + Clone {
custom(|inp| {
let savepoint = inp.save();
inp.rewind(savepoint);
Ok(())
})
.ignore_then(choice((
just(".string.")
.ignore_then(DelimiterKind::name_parser())
.map(CssState::String),
just(".multiline_comment").to(CssState::MultiLineComment),
just(".line_comment").to(CssState::LineComment),
empty().to(CssState::Regular),
)))
}
}
impl CssState {
fn parser<'src>(&self) -> impl Parser<'src, &'src str, CssState> {
match self {
Self::Regular => custom(|inp| parse_css(inp).map_err(|_| EmptyErr::default())).boxed(),
Self::String(delimiter) => {
let delim = *delimiter;
custom(move |inp| {
if parse_css_string(
inp,
one_of(match delim {
DelimiterKind::Bare => ")",
DelimiterKind::Double => "\"",
DelimiterKind::Single => "'",
})
.ignored(),
) {
Ok(CssState::String(delim))
} else {
parse_css(inp).map_err(|_| EmptyErr::default())
}
})
.boxed()
}
Self::MultiLineComment => custom(|inp| {
if parse_css_comment(inp, just("*/").ignored()) {
Ok(CssState::MultiLineComment)
} else {
parse_css(inp).map_err(|_| EmptyErr::default())
}
})
.boxed(),
Self::LineComment => custom(|inp| {
if parse_css_comment(inp, one_of("\n\x0C\r").ignored()) {
Ok(CssState::LineComment)
} else {
parse_css(inp).map_err(|_| EmptyErr::default())
}
})
.boxed(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum HtmlState {
Regular,
Tag(TagState<NoopInnerState>),
ScriptTag(TagState<ScriptState>),
StyleTag(TagState<CssState>),
TitleTag(TagState<NoopInnerState>),
TextAreaTag(TagState<NoopInnerState>),
Comment,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ScriptState {
Js(JsState),
UnknownScriptState(UnknownScriptState),
}
impl HasEmptyStategy for ScriptState {
fn empty_strategy(&self) -> EmptyStrategy {
match self {
Self::Js(js) => js.empty_strategy(),
Self::UnknownScriptState(_) => EmptyStrategy::None,
}
}
}
impl HasMultiLineStrategy for ScriptState {
fn multi_line_strategy(&self) -> MultiLineStrategy {
match self {
Self::Js(js) => js.multi_line_strategy(),
Self::UnknownScriptState(_) => MultiLineStrategy::None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct UnknownScriptState;
impl NameParser for UnknownScriptState {
fn name_parser<'src>() -> impl Parser<'src, &'src str, Self> + Clone {
empty().to(Self)
}
}
impl NameParser for ScriptState {
fn name_parser<'src>() -> impl Parser<'src, &'src str, Self> + Clone {
JsState::name_parser()
.map(ScriptState::Js)
.or(empty().to(ScriptState::UnknownScriptState(UnknownScriptState)))
}
}
impl Display for ScriptState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownScriptState(UnknownScriptState) => Ok(()),
Self::Js(js) => Display::fmt(js, f),
}
}
}
impl ScriptState {
fn parser<'src>(&self) -> impl Parser<'src, &'src str, ScriptState> {
match self {
Self::UnknownScriptState(UnknownScriptState) => any()
.repeated()
.to(ScriptState::UnknownScriptState(UnknownScriptState))
.boxed(),
Self::Js(js) => js.parser().map(ScriptState::Js).boxed(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct NoopInnerState;
impl HasEmptyStategy for NoopInnerState {
fn empty_strategy(&self) -> EmptyStrategy {
EmptyStrategy::None
}
}
impl HasMultiLineStrategy for NoopInnerState {
fn multi_line_strategy(&self) -> MultiLineStrategy {
MultiLineStrategy::None
}
}
impl NameParser for NoopInnerState {
fn name_parser<'src>() -> impl Parser<'src, &'src str, Self> + Clone {
any()
.filter(|_| false)
.map(|_| unreachable!("a noop inner state can never be parsed"))
}
}
impl Display for NoopInnerState {
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
impl Display for HtmlState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Regular => Ok(()),
Self::Tag(state) => write!(f, ".tag{}", state.sub_state),
Self::ScriptTag(
state @ TagState {
type_attr_value_is_js_or_unspecified: true,
..
},
) => {
write!(f, ".js_script_tag{}", state.sub_state)
}
Self::ScriptTag(state) => write!(f, ".unknown_script_tag{}", state.sub_state),
Self::StyleTag(state) => write!(f, ".style_tag{}", state.sub_state),
Self::TitleTag(state) => write!(f, ".title_tag{}", state.sub_state),
Self::TextAreaTag(state) => write!(f, ".text_area_tag{}", state.sub_state),
Self::Comment => f.write_str(".comment"),
}
}
}
fn parse_html_special_tag_contents<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
end_text: &'static str,
) -> Result<(&'s str, bool), EmptyErr> {
let tag_end = just_case_insensitive(end_text);
inp.parse(
just("\\<")
.ignored()
.or(any().and_is(tag_end.not()).ignored())
.repeated()
.to_slice()
.then(
tag_end
.then(html_whitespace())
.then(just('>'))
.to(false)
.or(end().to(true)),
),
)
}
impl HtmlState {
fn name_parser<'src>(
root_name_parser: impl Parser<'src, &'src str, ContextualizerState> + Clone,
) -> impl Parser<'src, &'src str, HtmlState> + Clone {
choice((
just(".tag")
.ignore_then(TagState::<NoopInnerState>::name_parser(
root_name_parser.clone(),
))
.map(HtmlState::Tag),
just(".js_script_tag")
.ignore_then(TagState::<JsState>::name_parser(root_name_parser.clone()))
.map(
|TagState {
type_attr_value_is_js_or_unspecified: _,
sub_state,
}| {
HtmlState::ScriptTag(TagState {
sub_state: match sub_state {
TagSubState::Attributes => TagSubState::Attributes,
TagSubState::AttributeName => TagSubState::AttributeName,
TagSubState::AttributeValue(inner, delim) => {
TagSubState::AttributeValue(inner, delim)
}
TagSubState::Inner(inner) => {
TagSubState::Inner(ScriptState::Js(inner))
}
},
type_attr_value_is_js_or_unspecified: true,
})
},
),
just(".unknown_script_tag")
.ignore_then(TagState::<UnknownScriptState>::name_parser(
root_name_parser.clone(),
))
.map(
|TagState {
type_attr_value_is_js_or_unspecified: _,
sub_state,
}| {
HtmlState::ScriptTag(TagState {
type_attr_value_is_js_or_unspecified: false,
sub_state: match sub_state {
TagSubState::Inner(UnknownScriptState) => TagSubState::Inner(
ScriptState::UnknownScriptState(UnknownScriptState),
),
TagSubState::Attributes => TagSubState::Attributes,
TagSubState::AttributeValue(inner, delim) => {
TagSubState::AttributeValue(inner, delim)
}
TagSubState::AttributeName => TagSubState::AttributeName,
},
})
},
),
just(".style_tag")
.ignore_then(TagState::<CssState>::name_parser(root_name_parser.clone()))
.map(HtmlState::StyleTag),
just(".title_tag")
.ignore_then(TagState::<NoopInnerState>::name_parser(
root_name_parser.clone(),
))
.map(HtmlState::TitleTag),
just(".text_area_tag")
.ignore_then(TagState::<NoopInnerState>::name_parser(
root_name_parser.clone(),
))
.map(HtmlState::TextAreaTag),
just(".comment").to(HtmlState::Comment),
empty().to(HtmlState::Regular),
))
}
#[allow(clippy::too_many_lines)]
fn parser<'src>(&self) -> impl Parser<'src, &'src str, HtmlState> {
fn special_parser<'src, T>(
end_text: &'static str,
wrapper: impl Fn(TagState<T>) -> HtmlState,
inner: impl Fn(&'src str) -> Result<T, EmptyErr>,
type_attr_value_is_js_or_unspecified: bool,
) -> impl Parser<'src, &'src str, HtmlState, extra::Default> {
custom(move |inp| {
let (content, ended_within) = parse_html_special_tag_contents(inp, end_text)?;
if ended_within {
return Ok(wrapper(TagState {
type_attr_value_is_js_or_unspecified,
sub_state: TagSubState::Inner(inner(content)?),
}));
}
parse_html(inp).map_err(|_| EmptyErr::default())
})
}
match self {
Self::Regular => custom(|inp| parse_html(inp).map_err(|_| EmptyErr::default())).boxed(),
Self::Tag(state) => {
let state = state.clone();
custom(
move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
TagStateParseRes::Inner(_) | TagStateParseRes::SelfClosing => {
parse_html(inp).map_err(|_| EmptyErr::default())
}
TagStateParseRes::State(state) => Ok(HtmlState::Tag(state.into())),
},
)
.boxed()
}
Self::ScriptTag(TagState {
type_attr_value_is_js_or_unspecified,
sub_state: TagSubState::Inner(state),
}) => {
let state = state.clone();
special_parser(
"</script",
HtmlState::ScriptTag,
move |content| {
state
.parser()
.parse(content)
.into_result()
.map_err(|_| EmptyErr::default())
},
*type_attr_value_is_js_or_unspecified,
)
.boxed()
}
Self::ScriptTag(state) => {
let state = state.clone();
custom(
move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
TagStateParseRes::Inner(type_attr_value_is_js_or_unspecified) => {
inp.parse(special_parser(
"</script",
HtmlState::ScriptTag,
|content| {
if type_attr_value_is_js_or_unspecified {
custom(|inp| Ok(ScriptState::Js(parse_js(inp, vec![]))))
.parse(content)
.into_result()
.map_err(|_| EmptyErr::default())
} else {
Ok(ScriptState::UnknownScriptState(UnknownScriptState))
}
},
type_attr_value_is_js_or_unspecified,
))
}
TagStateParseRes::SelfClosing => {
parse_html(inp).map_err(|_| EmptyErr::default())
}
TagStateParseRes::State(state) => Ok(HtmlState::ScriptTag(state.into())),
},
)
.boxed()
}
Self::StyleTag(TagState {
type_attr_value_is_js_or_unspecified: _,
sub_state: TagSubState::Inner(state),
}) => {
let state = state.clone();
special_parser(
"</style",
HtmlState::StyleTag,
move |content| {
state
.parser()
.parse(content)
.into_result()
.map_err(|_| EmptyErr::default())
},
true,
)
.boxed()
}
Self::StyleTag(state) => {
let state = state.clone();
custom(
move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
TagStateParseRes::Inner(_type_attr_value) => inp.parse(special_parser(
"</style",
HtmlState::StyleTag,
|content| {
custom(|inp| parse_css(inp).map_err(|_| EmptyErr::default()))
.parse(content)
.into_result()
.map_err(|_| EmptyErr::default())
},
true,
)),
TagStateParseRes::SelfClosing => {
parse_html(inp).map_err(|_| EmptyErr::default())
}
TagStateParseRes::State(state) => Ok(HtmlState::StyleTag(state.into())),
},
)
.boxed()
}
Self::TitleTag(state) => {
let state = state.clone();
custom(
move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
TagStateParseRes::Inner(_type_attr_value) => inp.parse(special_parser(
"</title",
HtmlState::TitleTag,
|_content| Ok(NoopInnerState),
state.type_attr_value_is_js_or_unspecified,
)),
TagStateParseRes::SelfClosing => {
parse_html(inp).map_err(|_| EmptyErr::default())
}
TagStateParseRes::State(state) => Ok(HtmlState::TitleTag(state.into())),
},
)
.boxed()
}
Self::TextAreaTag(state) => {
let state = state.clone();
custom(
move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
TagStateParseRes::Inner(_type_attr_value) => inp.parse(special_parser(
"</textarea",
HtmlState::TextAreaTag,
|_content| Ok(NoopInnerState),
true,
)),
TagStateParseRes::SelfClosing => {
parse_html(inp).map_err(|_| EmptyErr::default())
}
TagStateParseRes::State(state) => Ok(HtmlState::TextAreaTag(state.into())),
},
)
.boxed()
}
Self::Comment => custom(|inp| {
if parse_html_comment(inp) {
Ok(HtmlState::Comment)
} else {
parse_html(inp).map_err(|_| EmptyErr::default())
}
})
.boxed(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct TagState<T> {
type_attr_value_is_js_or_unspecified: bool,
sub_state: TagSubState<T>,
}
impl<T: HasEmptyStategy> HasEmptyStategy for TagState<T> {
fn empty_strategy(&self) -> EmptyStrategy {
self.sub_state.empty_strategy()
}
}
impl<T: HasMultiLineStrategy> HasMultiLineStrategy for TagState<T> {
fn multi_line_strategy(&self) -> MultiLineStrategy {
self.sub_state.multi_line_strategy()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct TagStateWithoutInner {
type_attr_value_is_js_or_unspecified: bool,
sub_state: TagSubStateWithoutInner,
}
impl<T> From<TagStateWithoutInner> for TagState<T> {
fn from(
TagStateWithoutInner {
type_attr_value_is_js_or_unspecified,
sub_state,
}: TagStateWithoutInner,
) -> Self {
Self {
type_attr_value_is_js_or_unspecified,
sub_state: sub_state.into(),
}
}
}
enum TagStateParseRes {
State(TagStateWithoutInner),
SelfClosing,
Inner(bool),
}
impl<T: NameParser + Clone> TagState<T> {
fn name_parser<'src>(
root_name_parser: impl Parser<'src, &'src str, ContextualizerState> + Clone,
) -> impl Parser<'src, &'src str, TagState<T>> + Clone {
TagSubState::name_parser(root_name_parser.clone()).map(|sub_state| TagState {
type_attr_value_is_js_or_unspecified: true,
sub_state,
})
}
fn parse<'src>(
&self,
inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
) -> Result<TagStateParseRes, ContextualizationError> {
match &self.sub_state {
TagSubState::AttributeName => {
let (_additional, end) = inp
.parse(parse_tag_attr_name(false))
.map_err(|_| ContextualizationError::InvalidStatic)?;
if end {
return Ok(TagStateParseRes::State(TagStateWithoutInner {
type_attr_value_is_js_or_unspecified: self
.type_attr_value_is_js_or_unspecified,
sub_state: TagSubStateWithoutInner::AttributeName,
}));
}
match parse_tag_attr_after_name(inp, None)? {
TagAttrRes::None => {
match parse_tag_inner(inp, self.type_attr_value_is_js_or_unspecified)? {
Left(state) => Ok(TagStateParseRes::State(state)),
Right((type_attr_value, false)) => {
Ok(TagStateParseRes::Inner(type_attr_value))
}
Right((_type_attr_value, true)) => Ok(TagStateParseRes::SelfClosing),
}
}
TagAttrRes::End(sub_state) => {
Ok(TagStateParseRes::State(TagStateWithoutInner {
type_attr_value_is_js_or_unspecified: self
.type_attr_value_is_js_or_unspecified,
sub_state,
}))
}
TagAttrRes::NewTypeAttrValueIsJs(value) => match parse_tag_inner(inp, value)? {
Left(state) => Ok(TagStateParseRes::State(state)),
Right((type_attr_value, false)) => {
Ok(TagStateParseRes::Inner(type_attr_value))
}
Right((_type_attr_value, true)) => Ok(TagStateParseRes::SelfClosing),
},
}
}
TagSubState::Attributes => {
match parse_tag_inner(inp, self.type_attr_value_is_js_or_unspecified)? {
Left(state) => Ok(TagStateParseRes::State(state)),
Right((type_attr_value_is_js_or_unspecified, false)) => Ok(
TagStateParseRes::Inner(type_attr_value_is_js_or_unspecified),
),
Right((_type_attr_value, true)) => Ok(TagStateParseRes::SelfClosing),
}
}
TagSubState::AttributeValue(ref inner_state, delim) => {
let parser = inner_state.parser();
let s = match delim {
DelimiterKind::Bare => inp.parse(attr_value_bare_parser()),
DelimiterKind::Double => inp.parse(attr_value_double_parser()),
DelimiterKind::Single => inp.parse(attr_value_single_parser()),
}
.expect(REPEATED_LOWER_BOUND_FAIL);
if inp
.parse(
end().to(true).or(match delim {
DelimiterKind::Bare => empty().boxed(),
DelimiterKind::Double => just('"').ignored().boxed(),
DelimiterKind::Single => just('\'').ignored().boxed(),
}
.to(false)),
)
.map_err(|_| ContextualizationError::InvalidStatic)?
{
let state = parser
.parse(s)
.into_result()
.map_err(|_| ContextualizationError::InvalidStatic)?;
return Ok(TagStateParseRes::State(TagStateWithoutInner {
type_attr_value_is_js_or_unspecified: self
.type_attr_value_is_js_or_unspecified,
sub_state: TagSubStateWithoutInner::AttributeValue(Box::new(state), *delim),
}));
}
match parse_tag_inner(inp, self.type_attr_value_is_js_or_unspecified)? {
Left(state) => Ok(TagStateParseRes::State(state)),
Right((type_attr_value, false)) => Ok(TagStateParseRes::Inner(type_attr_value)),
Right((_type_attr_value, true)) => Ok(TagStateParseRes::SelfClosing),
}
}
TagSubState::Inner(_t) => Ok(TagStateParseRes::Inner(true)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum TagSubState<T> {
Attributes,
AttributeName,
AttributeValue(Box<ContextualizerState>, DelimiterKind),
Inner(T),
}
impl<T: HasEmptyStategy> HasEmptyStategy for TagSubState<T> {
fn empty_strategy(&self) -> EmptyStrategy {
match self {
Self::AttributeValue(inner, DelimiterKind::Bare) => {
inner.empty_strategy().max(EmptyStrategy::Deny)
}
Self::AttributeValue(inner, _) => inner.empty_strategy(),
Self::Inner(inner) => inner.empty_strategy(),
_ => EmptyStrategy::None,
}
}
}
impl<T: HasMultiLineStrategy> HasMultiLineStrategy for TagSubState<T> {
fn multi_line_strategy(&self) -> MultiLineStrategy {
match self {
Self::AttributeValue(_inner, DelimiterKind::Bare) => MultiLineStrategy::Condense,
Self::AttributeValue(inner, _) => inner.multi_line_strategy(),
Self::AttributeName => MultiLineStrategy::Condense,
Self::Attributes => MultiLineStrategy::None,
Self::Inner(inner) => inner.multi_line_strategy(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum TagSubStateWithoutInner {
Attributes,
AttributeName,
AttributeValue(Box<ContextualizerState>, DelimiterKind),
}
impl<T> From<TagSubStateWithoutInner> for TagSubState<T> {
fn from(value: TagSubStateWithoutInner) -> Self {
match value {
TagSubStateWithoutInner::Attributes => Self::Attributes,
TagSubStateWithoutInner::AttributeName => Self::AttributeName,
TagSubStateWithoutInner::AttributeValue(inner, delim) => {
Self::AttributeValue(inner, delim)
}
}
}
}
impl<T: Display> Display for TagSubState<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Attributes => f.write_str(".attrs"),
Self::AttributeName => f.write_str(".attr_name"),
Self::AttributeValue(inner, delim) => write!(f, ".attr_value.{delim}.{inner}"),
Self::Inner(t) => t.fmt(f),
}
}
}
trait NameParser: Sized {
fn name_parser<'src>() -> impl Parser<'src, &'src str, Self> + Clone;
}
impl<T: NameParser + Clone> TagSubState<T> {
fn name_parser<'src>(
root_name_parser: impl Parser<'src, &'src str, ContextualizerState> + Clone,
) -> impl Parser<'src, &'src str, TagSubState<T>> + Clone {
choice((
just(".attrs").to(TagSubState::Attributes),
just(".attr_name").to(TagSubState::AttributeName),
just(".attr_value.")
.ignore_then(DelimiterKind::name_parser())
.then_ignore(just('.'))
.then(root_name_parser)
.map(|(delim, state)| TagSubState::AttributeValue(Box::new(state), delim)),
T::name_parser().map(TagSubState::Inner),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct JsState {
tmpl_literal_brace_counts: Vec<usize>,
sub_state: JsSubState,
}
impl HasEmptyStategy for JsState {
fn empty_strategy(&self) -> EmptyStrategy {
match self.sub_state {
JsSubState::Regex { in_charset: false } => EmptyStrategy::ReplaceRegex,
_ => EmptyStrategy::None,
}
}
}
impl HasMultiLineStrategy for JsState {
fn multi_line_strategy(&self) -> MultiLineStrategy {
match self.sub_state {
JsSubState::Regex { in_charset: _ }
| JsSubState::LineComment
| JsSubState::StringDouble
| JsSubState::StringSingle => MultiLineStrategy::Condense,
_ => MultiLineStrategy::None,
}
}
}
impl Display for JsState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.tmpl_literal_brace_counts.is_empty() {
Display::fmt(&self.sub_state, f)
} else {
let braces = self.tmpl_literal_brace_counts.iter().join(".");
write!(f, ".{braces}{}", self.sub_state)
}
}
}
impl NameParser for JsState {
fn name_parser<'src>() -> impl Parser<'src, &'src str, JsState> + Clone {
just(".")
.ignore_then(
int(10)
.try_map(|int: &'src str, _span| {
int.parse::<usize>()
.map_err(|_| EmptyErr::default())
.and_then(|i| (i != 0).then_some(i).ok_or(EmptyErr::default()))
})
.separated_by(just("."))
.at_least(1)
.collect::<Vec<usize>>(),
)
.or(empty().to(vec![]))
.then(JsSubState::name_parser())
.map(|(tmpl_literal_brace_counts, sub_state)| JsState {
tmpl_literal_brace_counts,
sub_state,
})
}
}
impl JsState {
fn parser<'src>(&self) -> impl Parser<'src, &'src str, JsState> {
let braces = self.tmpl_literal_brace_counts.clone();
match self.sub_state {
JsSubState::Regular => custom(move |inp| Ok(parse_js(inp, braces.clone()))).boxed(),
JsSubState::StringDouble => custom(move |inp| {
Ok(if inp.parse(js_string_parser('"'))? {
JsState {
tmpl_literal_brace_counts: braces.clone(),
sub_state: JsSubState::StringDouble,
}
} else {
parse_js(inp, braces.clone())
})
})
.boxed(),
JsSubState::StringSingle => custom(move |inp| {
Ok(if inp.parse(js_string_parser('\''))? {
JsState {
tmpl_literal_brace_counts: braces.clone(),
sub_state: JsSubState::StringSingle,
}
} else {
parse_js(inp, braces.clone())
})
})
.boxed(),
JsSubState::Regex { in_charset } => custom(move |inp| {
Ok(
if let Some(state) = parse_js_regex(inp, braces.clone(), in_charset) {
state
} else {
parse_js(inp, braces.clone())
},
)
})
.boxed(),
JsSubState::TmplLit(state) => custom(move |inp| {
Ok(match parse_js_tmpl_literal(inp, state) {
JsTmplLiteralItem::End(state) => JsState {
tmpl_literal_brace_counts: braces.clone(),
sub_state: JsSubState::TmplLit(state),
},
JsTmplLiteralItem::ExpressionStart => {
let mut new_braces = braces.clone();
new_braces.push(1);
parse_js(inp, new_braces)
}
JsTmplLiteralItem::LiteralEnd => parse_js(inp, braces.clone()),
})
})
.boxed(),
JsSubState::LineComment => custom(move |inp| {
Ok(
if let Some(state) = parse_js_line_comment(inp, braces.clone()) {
state
} else {
parse_js(inp, braces.clone())
},
)
})
.boxed(),
JsSubState::MultiLineComment => custom(move |inp| {
Ok(
if let Some(state) = parse_js_multi_line_comment(inp, braces.clone()) {
state
} else {
parse_js(inp, braces.clone())
},
)
})
.boxed(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum JsSubState {
Regular,
StringDouble,
StringSingle,
Regex { in_charset: bool },
TmplLit(TmplLitState),
LineComment,
MultiLineComment,
}
impl Display for JsSubState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Regular => Ok(()),
Self::StringDouble => f.write_str(".string.d"),
Self::StringSingle => f.write_str(".string.s"),
Self::Regex { in_charset: true } => f.write_str(".regex.charset"),
Self::Regex { in_charset: false } => f.write_str(".regex"),
Self::TmplLit(inner) => write!(f, ".tmpl_lit{inner}"),
Self::LineComment => f.write_str(".line_comment"),
Self::MultiLineComment => f.write_str(".multiline_comment"),
}
}
}
impl JsSubState {
fn name_parser<'src>() -> impl Parser<'src, &'src str, JsSubState> + Clone {
choice((
just(".string.d").to(JsSubState::StringDouble),
just(".string.s").to(JsSubState::StringSingle),
just(".regex.charset").to(JsSubState::Regex { in_charset: true }),
just(".regex").to(JsSubState::Regex { in_charset: false }),
just(".tmpl_lit")
.ignore_then(TmplLitState::name_parser())
.map(JsSubState::TmplLit),
just(".line_comment").to(JsSubState::LineComment),
just(".multiline_comment").to(JsSubState::MultiLineComment),
empty().to(JsSubState::Regular),
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TmplLitState {
Regular,
DollarSign,
}
impl Display for TmplLitState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Regular => Ok(()),
Self::DollarSign => f.write_str(".dollar_sign"),
}
}
}
impl TmplLitState {
fn name_parser<'src>() -> impl Parser<'src, &'src str, TmplLitState> + Clone {
choice((
just(".dollar_sign").to(TmplLitState::DollarSign),
empty().to(TmplLitState::Regular),
))
}
}
const REPEATED_LOWER_BOUND_FAIL: &str = "repeated without lower bound cannot fail";
const INCLUDED_END_FAIL: &str = "cannot fail since end is included";
fn html_whitespace<'src>(
) -> Repeated<OneOf<&'static str, &'src str, extra::Default>, char, &'src str, extra::Default> {
one_of(" \t\n\x0C\r").repeated()
}
#[allow(clippy::too_many_lines)]
fn parse_html<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
) -> Result<HtmlState, ContextualizationError> {
#[derive(Debug, Clone, Copy)]
enum TagOpeningKind {
Comment,
Closing,
Script,
Style,
TextArea,
Title,
Regular,
}
let tag_start = just('<');
let tag = any()
.filter(|c: &char| c.is_ascii_alphabetic())
.then(
any()
.filter(|c: &char| c.is_ascii_alphanumeric())
.repeated()
.at_least(1)
.separated_by(one_of(":-")),
)
.to_slice()
.then_ignore(end().not());
loop {
let Some(tag) = inp
.parse(
any()
.ignored()
.and_is(tag_start.not())
.or(just_case_insensitive("<!doctype")
.then(html_whitespace().at_least(1))
.then(just_case_insensitive("html"))
.then(html_whitespace())
.then(just(">"))
.ignored())
.repeated()
.ignore_then(
end().to(None).or(tag_start
.ignore_then(choice((
just("!--").to(TagOpeningKind::Comment),
just("/")
.then(tag)
.then(html_whitespace())
.then(just(">"))
.to(TagOpeningKind::Closing),
tag.map(|t: &str| match t.to_lowercase().as_str() {
"script" => TagOpeningKind::Script,
"style" => TagOpeningKind::Style,
"textarea" => TagOpeningKind::TextArea,
"title" => TagOpeningKind::Title,
_ => TagOpeningKind::Regular,
}),
)))
.map(Some)),
),
)
.map_err(|_| ContextualizationError::InvalidStatic)?
else {
return Ok(HtmlState::Regular);
};
match tag {
TagOpeningKind::Comment => {
if parse_html_comment(inp) {
return Ok(HtmlState::Comment);
}
}
TagOpeningKind::Closing => (),
TagOpeningKind::Regular => match parse_tag_inner(inp, true)? {
Left(state) => return Ok(HtmlState::Tag(state.into())),
Right((_type_attr_value, _self_closing)) => (),
},
TagOpeningKind::TextArea => match parse_tag_inner(inp, true)? {
Left(state) => return Ok(HtmlState::TextAreaTag(state.into())),
Right((_type_attr_value, self_closing)) => {
if self_closing {
continue;
}
let (_content, ended_within) =
parse_html_special_tag_contents(inp, "</textarea")
.map_err(|_| ContextualizationError::InvalidStatic)?;
if !ended_within {
return Ok(HtmlState::TextAreaTag(TagState {
type_attr_value_is_js_or_unspecified: true,
sub_state: TagSubState::Inner(NoopInnerState),
}));
}
}
},
TagOpeningKind::Title => match parse_tag_inner(inp, true)? {
Left(state) => return Ok(HtmlState::TitleTag(state.into())),
Right((_type_attr_value, self_closing)) => {
if self_closing {
continue;
}
let (_content, ended_within) = parse_html_special_tag_contents(inp, "</title")
.map_err(|_| ContextualizationError::InvalidStatic)?;
if ended_within {
return Ok(HtmlState::TitleTag(TagState {
type_attr_value_is_js_or_unspecified: true,
sub_state: TagSubState::Inner(NoopInnerState),
}));
}
}
},
TagOpeningKind::Script => match parse_tag_inner(inp, true)? {
Left(state) => return Ok(HtmlState::ScriptTag(state.into())),
Right((is_js, self_closing)) => {
if self_closing {
continue;
}
let (content, ended_within) = parse_html_special_tag_contents(inp, "</script")
.map_err(|_| ContextualizationError::InvalidStatic)?;
if !ended_within {
continue;
}
return Ok(if is_js {
let state = custom(|inp| Ok(parse_js(inp, vec![])))
.parse(content)
.into_result()
.map_err(|_| ContextualizationError::InvalidStatic)?;
HtmlState::ScriptTag(TagState {
type_attr_value_is_js_or_unspecified: true,
sub_state: TagSubState::Inner(ScriptState::Js(state)),
})
} else {
HtmlState::ScriptTag(TagState {
type_attr_value_is_js_or_unspecified: false,
sub_state: TagSubState::Inner(ScriptState::UnknownScriptState(
UnknownScriptState,
)),
})
});
}
},
TagOpeningKind::Style => match parse_tag_inner(inp, true)? {
Left(state) => return Ok(HtmlState::StyleTag(state.into())),
Right((_type_attr_value, self_closing)) => {
if self_closing {
continue;
}
let (content, ended_within) =
parse_html_special_tag_contents(inp, "</style")
.map_err(|_| ContextualizationError::InvalidStatic)?;
if !ended_within {
continue;
}
let state = custom(|inp| Ok(parse_css(inp)))
.parse(content)
.into_result()
.map_err(|_| ContextualizationError::InvalidStatic)??;
return Ok(HtmlState::StyleTag(TagState {
type_attr_value_is_js_or_unspecified: true,
sub_state: TagSubState::Inner(state),
}));
}
},
}
}
}
fn parse_html_comment<'s>(inp: &mut InputRef<'s, '_, &'s str, extra::Default>) -> bool {
let comment_end = just("-->");
inp.parse(
any()
.and_is(comment_end.not())
.repeated()
.ignore_then(comment_end.to(false).or(end().to(true))),
)
.expect(INCLUDED_END_FAIL)
}
fn parse_tag_attr_name<'s>(standalone: bool) -> impl Parser<'s, &'s str, (&'s str, bool)> {
let end_chars = one_of(" \t\n\x0C\r=>");
let illegal_chars = one_of("'\"<");
any()
.and_is(end_chars.or(illegal_chars).not())
.repeated()
.at_least(standalone.into())
.to_slice()
.then(end().or_not().map(|end| end.is_some()))
}
#[allow(clippy::type_complexity)]
fn parse_tag_inner<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
mut type_attr_value_is_js_or_unspecified: bool,
) -> Result<Either<TagStateWithoutInner, (bool, bool)>, ContextualizationError> {
let tag_end = just('>');
let end_chars = one_of(" \t\n\u{C}\r=>");
let illegal_chars = one_of("'\"<");
let self_closing = loop {
#[derive(Debug, Clone, Copy)]
enum TagContinuation<'s> {
End,
TagEnd,
SelfClosing,
Attribute(&'s str, bool),
Illegal,
}
let cont = inp
.parse(
html_whitespace().ignore_then(choice((
end().to(TagContinuation::End),
tag_end.to(TagContinuation::TagEnd),
just('/')
.then(html_whitespace())
.then(tag_end)
.to(TagContinuation::SelfClosing),
any()
.and_is(end_chars.or(illegal_chars).not())
.repeated()
.then(illegal_chars)
.to(TagContinuation::Illegal),
parse_tag_attr_name(true)
.map(|(attr_name, end)| TagContinuation::Attribute(attr_name, end)),
))),
)
.map_err(|_| ContextualizationError::InvalidStatic)?;
match cont {
TagContinuation::End => {
return Ok(Left(TagStateWithoutInner {
type_attr_value_is_js_or_unspecified,
sub_state: TagSubStateWithoutInner::Attributes,
}))
}
TagContinuation::TagEnd => break false,
TagContinuation::SelfClosing => break true,
TagContinuation::Illegal => return Err(ContextualizationError::InvalidStatic),
TagContinuation::Attribute(attr, end) => {
if end {
return Ok(Left(TagStateWithoutInner {
type_attr_value_is_js_or_unspecified,
sub_state: TagSubStateWithoutInner::AttributeName,
}));
}
match parse_tag_attr_after_name(inp, Some(attr))? {
TagAttrRes::None => (),
TagAttrRes::NewTypeAttrValueIsJs(new_type_attr_value) => {
type_attr_value_is_js_or_unspecified = new_type_attr_value;
}
TagAttrRes::End(sub_state) => {
return Ok(Left(TagStateWithoutInner {
type_attr_value_is_js_or_unspecified,
sub_state,
}))
}
}
}
}
};
Ok(Right((type_attr_value_is_js_or_unspecified, self_closing)))
}
enum TagAttrRes {
None,
NewTypeAttrValueIsJs(bool),
End(TagSubStateWithoutInner),
}
fn parse_tag_attr_after_name<'src>(
inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
attr: Option<&'src str>,
) -> Result<TagAttrRes, ContextualizationError> {
if !inp
.parse(
just('=')
.padded_by(html_whitespace())
.or_not()
.map(|eq| eq.is_some()),
)
.map_err(|_| ContextualizationError::InvalidStatic)?
{
return Ok(TagAttrRes::None);
}
let attr = attr.map(str::to_lowercase);
let attr_kind: AttributeKind = attr.as_deref().map_or(AttributeKind::Unsafe, Into::into);
let (value, delim) = parse_attr_value(inp)?;
let state = custom(|inp| match attr_kind {
AttributeKind::Css => parse_css(inp)
.map(ContextualizerState::Css)
.map_err(|_| EmptyErr::default()),
AttributeKind::Html => parse_html(inp)
.map(ContextualizerState::Html)
.map_err(|_| EmptyErr::default()),
AttributeKind::Js => Ok(ContextualizerState::Js(parse_js(inp, vec![]))),
AttributeKind::Plain => {
inp.parse(any().repeated())
.expect(REPEATED_LOWER_BOUND_FAIL);
Ok(ContextualizerState::PlainText)
}
AttributeKind::Unsafe => {
inp.parse(any().repeated())
.expect(REPEATED_LOWER_BOUND_FAIL);
Ok(ContextualizerState::Unsafe)
}
})
.parse(&value)
.into_result()
.map_err(|_| ContextualizationError::InvalidStatic)?;
if let Some(delim) = delim {
return Ok(TagAttrRes::End(TagSubStateWithoutInner::AttributeValue(
Box::new(state),
delim,
)));
}
Ok(if attr.is_some_and(|attr| &attr == "type") {
TagAttrRes::NewTypeAttrValueIsJs(is_js(&value))
} else {
TagAttrRes::None
})
}
fn js_whitespace<'src>() -> impl Parser<'src, &'src str, (), extra::Default> + Copy {
one_of("\u{C}\n\r\t\x0B\u{0020}\u{00a0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200a}\u{2028}\u{2029}\u{202f}\u{205f}\u{3000}\u{feff}").repeated()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum JsTmplLiteralItem {
ExpressionStart,
LiteralEnd,
End(TmplLitState),
}
fn parse_js_tmpl_literal<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
mut state: TmplLitState,
) -> JsTmplLiteralItem {
loop {
let item = choice((
match state {
TmplLitState::Regular => just('$'),
TmplLitState::DollarSign => just('{'),
}
.to(None),
just('`').to(JsTmplLiteralItem::LiteralEnd).map(Some),
end().to(state).map(JsTmplLiteralItem::End).map(Some),
));
match state {
TmplLitState::Regular => {
match inp
.parse(
just('\\')
.then(any())
.ignored()
.or(any().and_is(item.not()).ignored())
.repeated()
.ignore_then(item),
)
.expect(INCLUDED_END_FAIL)
{
Some(item) => return item,
None => state = TmplLitState::DollarSign,
}
}
TmplLitState::DollarSign => match inp.parse(item.or_not()).expect(INCLUDED_END_FAIL) {
Some(Some(item)) => return item,
Some(None) => return JsTmplLiteralItem::ExpressionStart,
None => state = TmplLitState::Regular,
},
}
}
}
#[allow(clippy::too_many_lines)]
fn parse_js<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
mut tmpl_literal_brace_counts: Vec<usize>,
) -> JsState {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum JsItem {
End,
Regex,
StringDouble,
StringSingle,
TmplLiteral,
LineComment,
MultiLineComment,
Curly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum JsItemOrCurly {
JsItem(JsItem),
Curly(CurlyKind),
}
fn handle_js_item<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
tmpl_literal_brace_counts: &mut Vec<usize>,
item: JsItem,
) -> Option<JsState> {
match item {
JsItem::End => {
return Some(JsState {
tmpl_literal_brace_counts: tmpl_literal_brace_counts.clone(),
sub_state: JsSubState::Regular,
});
}
JsItem::Regex => {
if let Some(state) = parse_js_regex(inp, tmpl_literal_brace_counts.clone(), false) {
return Some(state);
}
}
JsItem::StringDouble => {
if inp.parse(js_string_parser('"')).expect(INCLUDED_END_FAIL) {
return Some(JsState {
tmpl_literal_brace_counts: tmpl_literal_brace_counts.clone(),
sub_state: JsSubState::StringDouble,
});
}
}
JsItem::StringSingle => {
if inp.parse(js_string_parser('\'')).expect(INCLUDED_END_FAIL) {
return Some(JsState {
tmpl_literal_brace_counts: tmpl_literal_brace_counts.clone(),
sub_state: JsSubState::StringSingle,
});
}
}
JsItem::TmplLiteral => match parse_js_tmpl_literal(inp, TmplLitState::Regular) {
JsTmplLiteralItem::ExpressionStart => {
tmpl_literal_brace_counts.push(1);
}
JsTmplLiteralItem::LiteralEnd => (),
JsTmplLiteralItem::End(state) => {
return Some(JsState {
tmpl_literal_brace_counts: tmpl_literal_brace_counts.clone(),
sub_state: JsSubState::TmplLit(state),
});
}
},
JsItem::LineComment => {
if let Some(state) = parse_js_line_comment(inp, tmpl_literal_brace_counts.clone()) {
return Some(state);
}
}
JsItem::MultiLineComment => {
if let Some(state) =
parse_js_multi_line_comment(inp, tmpl_literal_brace_counts.clone())
{
return Some(state);
}
}
JsItem::Curly => (),
}
None
}
let js_item = choice((
end().to(JsItem::End),
choice((
just('+')
.repeated()
.at_least(1)
.count()
.filter(|count| count % 2 == 1)
.or(just('-')
.repeated()
.at_least(1)
.count()
.filter(|count| count % 2 == 1))
.ignored(),
any()
.filter(|char: &char| !char.is_ascii_digit())
.then(just('.'))
.ignored(),
one_of(",<>=*%&|^?!~([:;{}").ignored(),
just("break").ignored(),
just("case").ignored(),
just("continue").ignored(),
just("delete").ignored(),
just("do").ignored(),
just("else").ignored(),
just("finally").ignored(),
just("in").ignored(),
just("instanceof").ignored(),
just("return").ignored(),
just("throw").ignored(),
just("try").ignored(),
just("typeof").ignored(),
just("void").ignored(),
))
.then(js_whitespace())
.then(just('/'))
.ignore_then(one_of("*/").ignored().or(end()).not())
.to(JsItem::Regex),
just('"').to(JsItem::StringDouble),
just("'").to(JsItem::StringSingle),
just("`").to(JsItem::TmplLiteral),
choice((just("//"), just("<!--"), just("-->"), just("#!"))).to(JsItem::LineComment),
just("/*").to(JsItem::MultiLineComment),
one_of("{}").to(JsItem::Curly),
));
let js_item_or_braces = just('{')
.to(JsItemOrCurly::Curly(CurlyKind::Opening))
.or(just('}').to(JsItemOrCurly::Curly(CurlyKind::Closing)))
.or(js_item.map(JsItemOrCurly::JsItem));
loop {
let prev = inp.save();
match inp
.parse(
any()
.and_is(js_item_or_braces.not())
.repeated()
.ignore_then(js_item_or_braces),
)
.expect(INCLUDED_END_FAIL)
{
JsItemOrCurly::JsItem(item) => {
if let Some(state) = handle_js_item(inp, &mut tmpl_literal_brace_counts, item) {
return state;
}
}
JsItemOrCurly::Curly(curly_kind) => {
let should_rewind = match curly_kind {
CurlyKind::Opening => {
if let Some(last) = tmpl_literal_brace_counts.last_mut() {
*last += 1;
}
true
}
CurlyKind::Closing => match tmpl_literal_brace_counts.last_mut() {
Some(1) => {
tmpl_literal_brace_counts.pop();
match parse_js_tmpl_literal(inp, TmplLitState::Regular) {
JsTmplLiteralItem::End(state) => {
return JsState {
tmpl_literal_brace_counts,
sub_state: JsSubState::TmplLit(state),
}
}
JsTmplLiteralItem::LiteralEnd => (),
JsTmplLiteralItem::ExpressionStart => {
tmpl_literal_brace_counts.push(1);
}
}
false
}
Some(last) => {
*last -= 1;
true
}
None => true,
},
};
if should_rewind {
inp.rewind(prev);
let item = inp
.parse(any().and_is(js_item.not()).repeated().ignore_then(js_item))
.expect(INCLUDED_END_FAIL);
if let Some(state) = handle_js_item(inp, &mut tmpl_literal_brace_counts, item) {
return state;
}
}
}
}
}
}
fn js_string_parser<'src>(c: char) -> impl Parser<'src, &'src str, bool, extra::Default> {
choice((
just('\\').then(any()).ignored(),
any().and_is(just(c).not()).ignored(),
))
.repeated()
.ignore_then(just(c).to(false).or(end().to(true)))
}
fn parse_js_regex<'src>(
inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
tmpl_literal_brace_counts: Vec<usize>,
in_charset: bool,
) -> Option<JsState> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RegexItem {
CharsetBegin,
CharsetEnd,
End,
RegexEnd,
}
let element = choice((
just('[').to(RegexItem::CharsetBegin),
just(']').to(RegexItem::CharsetEnd),
just('/').to(RegexItem::RegexEnd),
end().to(RegexItem::End),
));
let mut in_charset = in_charset;
loop {
match inp
.parse(
just('\\')
.then(any())
.ignored()
.or(any().and_is(element.not()).ignored())
.repeated()
.ignore_then(element),
)
.expect(INCLUDED_END_FAIL)
{
RegexItem::CharsetBegin => in_charset = true,
RegexItem::CharsetEnd => in_charset = false,
RegexItem::RegexEnd => break None,
RegexItem::End => {
return Some(JsState {
tmpl_literal_brace_counts,
sub_state: JsSubState::Regex { in_charset },
})
}
}
}
}
fn parse_js_line_comment<'src>(
inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
tmpl_literal_brace_counts: Vec<usize>,
) -> Option<JsState> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CommentItem {
End,
Newline,
}
let p = end()
.to(CommentItem::End)
.or(newline().to(CommentItem::Newline));
let p = any().and_is(p.not()).repeated().ignore_then(p);
match inp.parse(p).expect(INCLUDED_END_FAIL) {
CommentItem::End => Some(JsState {
tmpl_literal_brace_counts,
sub_state: JsSubState::LineComment,
}),
CommentItem::Newline => None,
}
}
fn parse_js_multi_line_comment<'src>(
inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
tmpl_literal_brace_counts: Vec<usize>,
) -> Option<JsState> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CommentItem {
End,
CommentEnd,
}
let p = end()
.to(CommentItem::End)
.or(just("*/").to(CommentItem::CommentEnd));
let p = any().and_is(p.not()).repeated().ignore_then(p);
match inp.parse(p).expect(INCLUDED_END_FAIL) {
CommentItem::End => Some(JsState {
tmpl_literal_brace_counts,
sub_state: JsSubState::MultiLineComment,
}),
CommentItem::CommentEnd => None,
}
}
fn is_js(mime: &str) -> bool {
let mime = mime
.split_once(';')
.map_or(mime, |(prefix, _suffix)| prefix)
.trim()
.to_lowercase();
matches!(
mime.as_str(),
"application/ecmascript"
| "application/javascript"
| "application/json"
| "application/ld+json"
| "application/x-ecmascript"
| "application/x-javascript"
| "module"
| "text/ecmascript"
| "text/javascript"
| "text/javascript1."
| "text/javascript1.1"
| "text/javascript1.2"
| "text/javascript1.3"
| "text/javascript1.4"
| "text/javascript1.5"
| "text/jscript"
| "text/livescript"
| "text/x-ecmascript"
| "text/x-javascript"
)
}
include!(concat!(env!("OUT_DIR"), "/entities_generated.rs"));
fn unescape_entites(s: &str) -> Result<Cow<'_, str>, ()> {
let unicode_entity = just::<_, _, extra::Default>("&#")
.ignore_then(
one_of("xX")
.ignore_then(int(16).try_map(|hex, _| {
u32::from_str_radix(hex, 16).map_err(|_| EmptyErr::default())
}))
.or(int(10)
.try_map(|decimal, _| str::parse(decimal).map_err(|_| EmptyErr::default()))),
)
.try_map(|code, _| char::from_u32(code).ok_or(EmptyErr::default()))
.map(|char| Cow::Owned(char.to_string()));
let named_entity = just('&')
.then(any().filter(char::is_ascii_alphanumeric).repeated())
.to_slice()
.try_map(|entity: &str, _| {
ENTITY_MAP
.get(&entity.to_lowercase())
.ok_or(EmptyErr::default())
.copied()
})
.map(Cow::Borrowed);
let entity = unicode_entity
.or(named_entity)
.then_ignore(just(';').or_not());
let parser = IterParser::collect::<Vec<Cow<str>>>(
entity
.or(any()
.and_is(entity.not())
.repeated()
.at_least(1)
.to_slice()
.map(Cow::Borrowed))
.repeated(),
);
let cows: Vec<Cow<str>> = parser.parse(s).into_result().map_err(|_| ())?;
Ok(match cows.len() {
0 => Cow::Borrowed(""),
1 => cows.into_iter().next().expect("vec has length 1"),
_ => {
let mut res = String::with_capacity(cows.iter().map(|cow| cow.len()).sum());
for cow in cows {
res.push_str(&cow);
}
Cow::Owned(res)
}
})
}
fn attr_value_single_parser<'src>() -> impl Parser<'src, &'src str, &'src str> + Copy {
any().and_is(just('\'').not()).repeated().to_slice()
}
fn attr_value_double_parser<'src>() -> impl Parser<'src, &'src str, &'src str> + Copy {
any().and_is(just('"').not()).repeated().to_slice()
}
fn attr_value_bare_parser<'src>() -> impl Parser<'src, &'src str, &'src str> + Copy {
any()
.and_is(html_whitespace().at_least(1).or(just('>').ignored()).not())
.repeated()
.to_slice()
}
fn parse_attr_value<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
) -> Result<(Cow<'s, str>, Option<DelimiterKind>), ContextualizationError> {
let ((delimiter, value), end) = inp
.parse(
choice((
just('\'').ignore_then(
attr_value_single_parser().map(|value| (DelimiterKind::Single, value)),
),
just('"').ignore_then(
attr_value_double_parser().map(|value| (DelimiterKind::Double, value)),
),
attr_value_bare_parser().map(|value| (DelimiterKind::Bare, value)),
))
.then(end().or_not().map(|end| end.is_some())),
)
.map_err(|_| ContextualizationError::InvalidStatic)?;
let value = unescape_entites(value).map_err(|()| ContextualizationError::InvalidStatic)?;
if end {
return Ok((value, true.then_some(delimiter)));
}
let end_char = match delimiter {
DelimiterKind::Bare => return Ok((value, end.then_some(delimiter))),
DelimiterKind::Single => '\'',
DelimiterKind::Double => '"',
};
inp.parse(just(end_char))
.map_err(|_| ContextualizationError::InvalidStatic)?;
Ok((value, end.then_some(delimiter)))
}
fn css_whitespace<'s>(
) -> Repeated<impl Parser<'s, &'s str, (), extra::Default> + Copy, (), &'s str, extra::Default> {
choice((
one_of(" \t\n\u{0C}").ignored(),
just("\r\n").ignored(),
just('\r').ignored(),
))
.repeated()
}
fn parse_css<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
) -> Result<CssState, ContextualizationError> {
#[derive(Clone, Copy)]
enum CssItem {
End,
Url(DelimiterKind),
LineComment,
MultiLineComment,
StringSingle,
StringDouble,
}
let item = choice((
end().to(CssItem::End),
empty()
.or(any().filter(|c: &char| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '\u{80}'..='\u{d7ff}' | '\u{e000}' ..= '\u{fffd}' | '\u{10000}' ..= '\u{10ffff}')).ignored())
.then(just_case_insensitive("url"))
.then(css_whitespace())
.then(just('('))
.then(css_whitespace())
.ignore_then(choice((
just('"').to(DelimiterKind::Double),
just('\'').to(DelimiterKind::Single),
empty().to(DelimiterKind::Bare),
)))
.map(CssItem::Url),
just("//").to(CssItem::LineComment),
just("/*").to(CssItem::MultiLineComment),
just('"').to(CssItem::StringDouble),
just('\'').to(CssItem::StringSingle),
));
loop {
match inp
.parse(any().and_is(item.not()).repeated().ignore_then(item))
.map_err(|_| ContextualizationError::InvalidStatic)?
{
CssItem::End => return Ok(CssState::Regular),
CssItem::Url(delim) => {
if parse_css_string(
inp,
one_of(match delim {
DelimiterKind::Bare => ")",
DelimiterKind::Double => "\"",
DelimiterKind::Single => "'",
})
.ignored(),
) {
return Ok(CssState::String(delim));
}
}
CssItem::StringDouble => {
if parse_css_string(inp, just('"').ignored()) {
return Ok(CssState::String(DelimiterKind::Double));
}
}
CssItem::StringSingle => {
if parse_css_string(inp, just('\'').ignored()) {
return Ok(CssState::String(DelimiterKind::Single));
}
}
CssItem::LineComment => {
if parse_css_comment(inp, one_of("\n\x0C\r").ignored()) {
return Ok(CssState::LineComment);
}
}
CssItem::MultiLineComment => {
if parse_css_comment(inp, just("*/").ignored()) {
return Ok(CssState::MultiLineComment);
}
}
}
}
}
fn parse_css_string<'s>(
inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
endp: impl Parser<'s, &'s str, (), extra::Default> + Clone,
) -> bool {
inp.parse(
just('\\')
.ignore_then(any())
.or(any().and_is(endp.clone().not()))
.repeated()
.ignore_then(endp.to(false).or(end().to(true))),
)
.expect(INCLUDED_END_FAIL)
}
fn parse_css_comment<'s, 'a>(
inp: &mut InputRef<'s, 'a, &'s str, extra::Default>,
endp: impl Parser<'s, &'s str, ()> + Copy,
) -> bool {
inp.parse(
any()
.and_is(endp.not())
.repeated()
.ignore_then(endp.to(false).or(end().to(true))),
)
.expect(INCLUDED_END_FAIL)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DelimiterKind {
Bare,
Single,
Double,
}
impl DelimiterKind {
fn name_parser<'src>() -> impl Parser<'src, &'src str, DelimiterKind> + Clone {
choice((
just('b').to(DelimiterKind::Bare),
just('s').to(DelimiterKind::Single),
just('d').to(DelimiterKind::Double),
))
}
}
impl Display for DelimiterKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
DelimiterKind::Bare => "b",
DelimiterKind::Single => "s",
DelimiterKind::Double => "d",
})
}
}
#[derive(Debug, Clone, Copy)]
enum AttributeKind {
Plain,
Html,
Css,
Js,
Unsafe,
}
impl From<&str> for AttributeKind {
fn from(value: &str) -> Self {
let value = if let Some(remainder) = value.strip_prefix("data-") {
remainder
} else if let Some((_prefix, remainder)) = value.split_once(':') {
remainder
} else {
value
};
match value {
"accept" | "alt" | "autocomplete" | "autofocus" | "autoplay" | "border" | "checked"
| "class" | "cols" | "colspan" | "contenteditable" | "contextmenu" | "controls"
| "coords" | "datetime" | "default" | "dir" | "dirname" | "disabled" | "draggable"
| "dropzone" | "for" | "formtarget" | "headers" | "height" | "hidden" | "high"
| "hreflang" | "id" | "ismap" | "kind" | "label" | "lang" | "list" | "loop" | "low"
| "max" | "maxlength" | "media" | "mediagroup" | "min" | "multiple" | "name"
| "open" | "optimum" | "placeholder" | "preload" | "pubdate" | "radiogroup"
| "readonly" | "required" | "reversed" | "rows" | "rowspan" | "spellcheck"
| "scope" | "scoped" | "seamless" | "selected" | "shape" | "size" | "sizes"
| "span" | "srclang" | "start" | "step" | "tabindex" | "target" | "title" | "width"
| "wrap" => AttributeKind::Plain,
"accept-charset" | "async" | "challenge" | "charset" | "content" | "crossorigin"
| "defer" | "enctype" | "form" | "formenctype" | "formmethod" | "formnovalidate"
| "http-equiv" | "keytype" | "language" | "method" | "novalidate" | "pattern"
| "rel" | "sandbox" | "type" | "value" => AttributeKind::Unsafe,
"srcdoc" => AttributeKind::Html,
"style" => AttributeKind::Css,
_ if value.starts_with("on") => AttributeKind::Js,
_ => AttributeKind::Plain,
}
}
}
type Replacement = (&'static str, &'static str);
type Replacements<'a> = &'a [Replacement];
fn merge_replacements<A: IntoIterator<Item = B>, B: IntoIterator<Item = Replacement>>(
replacements: A,
) -> AhoCorasickEscaper {
let (mut total_patterns, mut total_replacements): (Vec<&'static str>, Vec<String>) =
(Vec::new(), Vec::new());
for replacements in replacements {
let (patterns, replacements) = replacements.into_iter().fold(
(Vec::new(), Vec::new()),
|(mut patterns, mut replacements), (pattern, replacement)| {
patterns.push(pattern);
replacements.push(replacement.to_string());
(patterns, replacements)
},
);
let current_aho_corasick = AhoCorasickBuilder::new()
.ascii_case_insensitive(true)
.build(&patterns)
.expect("failed to build intermediate aho corasick");
for replacement in &mut total_replacements {
*replacement = current_aho_corasick.replace_all(replacement, &replacements);
}
total_patterns.extend(patterns);
total_replacements.extend(replacements);
}
let searcher = AhoCorasickBuilder::new()
.ascii_case_insensitive(true)
.build(total_patterns)
.expect("failed to build final aho corasick");
AhoCorasickEscaper {
searcher,
replacements: total_replacements,
}
}
#[cfg(test)]
mod test_merge_replacements {
use super::{merge_replacements, Replacements};
use pretty_assertions::assert_eq;
fn test_escape(replacements: &[Replacements], inp: &'static str, expected_out: &'static str) {
assert_eq!(
merge_replacements(
replacements
.iter()
.copied()
.map(|slice| slice.iter().copied())
)
.escape(inp.to_string()),
expected_out.to_string()
);
}
#[test]
fn test_simple() {
test_escape(
&[&[("a", "b"), ("e", "i")]],
"hello alberta!",
"hillo blbirtb!",
);
}
#[test]
fn test_unrelated() {
test_escape(
&[&[("a", "b"), ("c", "d")], &[("e", "f"), ("h", "i")]],
"abcdefghij",
"bbddffgiij",
);
}
#[test]
fn test_chained() {
test_escape(
&[
&[("hello", "much bye"), ("max", "otto")],
&[("much", "very"), ("tt", "mm")],
],
"hello max",
"very bye ommo",
);
}
}
const IE_BAD_CHARS_REPLACEMENTS: Replacements = &[
("\u{fdd0}", ""),
("\u{fdd1}", ""),
("\u{fdd2}", ""),
("\u{fdd3}", ""),
("\u{fdd4}", ""),
("\u{fdd5}", ""),
("\u{fdd6}", ""),
("\u{fdd7}", ""),
("\u{fdd8}", ""),
("\u{fdd9}", ""),
("\u{fdda}", ""),
("\u{fddb}", ""),
("\u{fddc}", ""),
("\u{fddd}", ""),
("\u{fdde}", ""),
("\u{fddf}", ""),
("\u{fde0}", ""),
("\u{fde1}", ""),
("\u{fde2}", ""),
("\u{fde3}", ""),
("\u{fde4}", ""),
("\u{fde5}", ""),
("\u{fde6}", ""),
("\u{fde7}", ""),
("\u{fde8}", ""),
("\u{fde9}", ""),
("\u{fdea}", ""),
("\u{fdeb}", ""),
("\u{fdec}", ""),
("\u{fded}", ""),
("\u{fdee}", ""),
("\u{fdef}", ""),
("\u{fff0}", "￰"),
("\u{fff1}", "￱"),
("\u{fff2}", "￲"),
("\u{fff3}", "￳"),
("\u{fff4}", "￴"),
("\u{fff5}", "￵"),
("\u{fff6}", "￶"),
("\u{fff7}", "￷"),
("\u{fff8}", "￸"),
("\u{fff9}", ""),
("\u{fffa}", ""),
("\u{fffb}", ""),
("\u{fffc}", ""),
("\u{fffd}", "�"),
("\u{fffe}", ""),
("\u{fffe}", ""),
];
const HTML_REPLACEMENTS: Replacements = &[
("\0", "\u{FFFD}"),
("&", "&"),
("+", "+"),
("<", "<"),
(">", ">"),
("'", "'"),
("\"", """),
];
const HTML_NO_SPACE_REPLACEMENTS: Replacements = &[
("\t", "	"),
("\n", " "),
("\u{B}", ""),
("\u{C}", ""),
("\r", " "),
(" ", " "),
("=", "="),
("`", "`"),
];
const CSS_STRING_REPLACEMENTS: Replacements = &[
("\0", "\\0 "),
("\t", "\\9 "),
("\n", "\\a "),
("\u{C}", "\\c "),
("\r", "\\d "),
("\"", "\\22 "),
("'", "\\27 "),
("\\", "\\\\"),
("<", "\\3c "),
(">", "\\3e "),
("(", "\\28 "),
(")", "\\29 "),
("&", "\\26 "),
("+", "\\2b "),
("/", "\\2f "),
(":", "\\3a "),
(";", "\\3b "),
("{", "\\7b "),
("}", "\\7d "),
];
const HTML_COMMENT_REPLACEMENTS: Replacements = &[("-->", "")];
const HTML_CSS_REPLACEMENTS: Replacements = &[("</style", "\\3c \\2f style")];
const CSS_LINE_COMMENT_REPLACEMENTS: Replacements = &[("\n", ""), ("\x0C", ""), ("\r", "")];
const CSS_MULTI_LINE_COMMENT_REPLACEMENTS: Replacements = &[("*/", "")];
const HTML_JS_REPLACEMENTS: Replacements = &[("</script", "\\3c \\2f script")];
const JS_STRING_NO_BACKSLASH_REPLACEMENTS: Replacements = &[
("\t", "\\t"),
("\n", "\\n"),
("\u{000B}", "\\u000b"),
("\u{000C}", "\\f"),
("\r", "\\r"),
("\"", "\\u0022"),
("`", "\\u0060"),
("'", "\\u0027"),
("&", "\\u0026"),
("+", "\\u002b"),
("/", "\\/"),
("<", "\\u003c"),
(">", "\\u003e"),
];
const JS_STRING_BACKSLASH_REPLACEMENTS: Replacements = &[("\\", "\\\\")];
const JS_REGEX_REPLACEMENTS: Replacements = &[
("$", "\\$"),
("(", "\\("),
(")", "\\)"),
("*", "\\*"),
("+", "\\u002b"),
("-", "\\-"),
(".", "\\."),
("?", "\\?"),
("[", "\\["),
("]", "\\]"),
("^", "\\^"),
("{", "\\{"),
("}", "\\}"),
("|", "\\|"),
];
const JS_TEMPLATE_LITERAL_REPLACEMENTS: Replacements =
&[("$", "\\u0024"), ("{", "\\u007b"), ("}", "\\u007d")];
impl HasEmptyStategy for ContextualizerState {
fn empty_strategy(&self) -> EmptyStrategy {
match self {
ContextualizerState::Html(HtmlState::ScriptTag(inner)) => inner.empty_strategy(),
ContextualizerState::Html(HtmlState::StyleTag(inner)) => inner.empty_strategy(),
ContextualizerState::Html(
HtmlState::TextAreaTag(inner) | HtmlState::Tag(inner) | HtmlState::TitleTag(inner),
) => inner.empty_strategy(),
ContextualizerState::Css(inner) => inner.empty_strategy(),
ContextualizerState::Js(inner) => inner.empty_strategy(),
_ => EmptyStrategy::None,
}
}
}
impl HasMultiLineStrategy for ContextualizerState {
fn multi_line_strategy(&self) -> MultiLineStrategy {
match self {
Self::Html(HtmlState::ScriptTag(inner)) => inner.multi_line_strategy(),
Self::Html(HtmlState::StyleTag(inner)) => inner.multi_line_strategy(),
ContextualizerState::Html(
HtmlState::TextAreaTag(inner) | HtmlState::Tag(inner) | HtmlState::TitleTag(inner),
) => inner.multi_line_strategy(),
ContextualizerState::Css(inner) => inner.multi_line_strategy(),
ContextualizerState::Js(inner) => inner.multi_line_strategy(),
_ => MultiLineStrategy::None,
}
}
}
impl ContextualizerState {
#[allow(clippy::too_many_lines)]
fn escape_stages(
&self,
other: &ContextualizerState,
) -> Result<Vec<Replacements<'static>>, ContextualizationError> {
macro_rules! attribute_value {
($inner:expr) => {{
let TagSubState::AttributeValue(ref inner, delim) = $inner.sub_state else {
unreachable!("due to match arm guard");
};
let mut res = inner.escape_stages(other)?;
match delim {
DelimiterKind::Bare => {
res.extend([
HTML_REPLACEMENTS,
HTML_NO_SPACE_REPLACEMENTS,
IE_BAD_CHARS_REPLACEMENTS,
]);
}
DelimiterKind::Single | DelimiterKind::Double => {
res.extend([HTML_REPLACEMENTS]);
}
}
res
}};
}
if self == other {
return Ok(vec![]);
}
Ok(match (self, other) {
(
ContextualizerState::Html(
HtmlState::Regular
| HtmlState::TitleTag(TagState {
sub_state: TagSubState::Inner(_),
..
})
| HtmlState::TextAreaTag(TagState {
sub_state: TagSubState::Inner(_),
..
}),
),
ContextualizerState::PlainText,
) => {
vec![HTML_REPLACEMENTS]
}
(
ContextualizerState::Html(
HtmlState::Tag(inner)
| HtmlState::TextAreaTag(inner)
| HtmlState::TitleTag(inner),
),
_,
) if matches!(inner.sub_state, TagSubState::AttributeValue(_, _)) => {
attribute_value!(inner)
}
(ContextualizerState::Html(HtmlState::Comment), ContextualizerState::PlainText) => {
vec![HTML_COMMENT_REPLACEMENTS]
}
(ContextualizerState::Html(HtmlState::StyleTag(inner)), _)
if matches!(inner.sub_state, TagSubState::AttributeValue(_, _)) =>
{
attribute_value!(inner)
}
(
ContextualizerState::Html(HtmlState::StyleTag(TagState {
sub_state: TagSubState::Inner(inner),
..
})),
ContextualizerState::PlainText | ContextualizerState::Css(_),
) => {
let mut res = vec![HTML_CSS_REPLACEMENTS];
let inner_self = ContextualizerState::Css(inner.clone());
res.extend(inner_self.escape_stages(other)?);
res
}
(ContextualizerState::Css(CssState::String(_)), ContextualizerState::PlainText) => {
vec![CSS_STRING_REPLACEMENTS]
}
(ContextualizerState::Css(CssState::LineComment), ContextualizerState::PlainText) => {
vec![CSS_LINE_COMMENT_REPLACEMENTS]
}
(
ContextualizerState::Css(CssState::MultiLineComment),
ContextualizerState::PlainText,
) => {
vec![CSS_MULTI_LINE_COMMENT_REPLACEMENTS]
}
(ContextualizerState::Html(HtmlState::ScriptTag(inner)), _)
if matches!(inner.sub_state, TagSubState::AttributeValue(_, _)) =>
{
attribute_value!(inner)
}
(
ContextualizerState::Html(HtmlState::ScriptTag(TagState {
sub_state: TagSubState::Inner(ScriptState::Js(inner)),
..
})),
ContextualizerState::PlainText | ContextualizerState::Js(_),
) => {
let mut res = vec![HTML_JS_REPLACEMENTS];
let inner_self = ContextualizerState::Js(inner.clone());
res.extend(inner_self.escape_stages(other)?);
res
}
(total_first @ ContextualizerState::Js(first), ContextualizerState::Js(second))
if first.tmpl_literal_brace_counts.is_empty()
&& !second.tmpl_literal_brace_counts.is_empty() =>
{
total_first.escape_stages(&ContextualizerState::Js(JsState {
tmpl_literal_brace_counts: vec![],
sub_state: second.sub_state.clone(),
}))?
}
(
ContextualizerState::Js(JsState {
tmpl_literal_brace_counts: first_braces,
sub_state: first_sub,
}),
ContextualizerState::Js(JsState {
tmpl_literal_brace_counts: second_braces,
sub_state: second_sub,
}),
) if !first_braces.is_empty()
&& !second_braces.is_empty()
&& first_braces
.iter()
.zip(second_braces.iter())
.all(|(first, second)| first >= second) =>
{
ContextualizerState::Js(JsState {
tmpl_literal_brace_counts: vec![],
sub_state: first_sub.clone(),
})
.escape_stages(&ContextualizerState::Js(JsState {
tmpl_literal_brace_counts: vec![],
sub_state: second_sub.clone(),
}))?
}
(
ContextualizerState::Js(JsState {
tmpl_literal_brace_counts: _,
sub_state: JsSubState::StringSingle | JsSubState::StringDouble,
}),
ContextualizerState::PlainText,
) => vec![
JS_STRING_BACKSLASH_REPLACEMENTS,
JS_STRING_NO_BACKSLASH_REPLACEMENTS,
],
(
ContextualizerState::Js(JsState {
tmpl_literal_brace_counts: _,
sub_state: JsSubState::Regex { in_charset: false },
}),
ContextualizerState::PlainText,
) => vec![
JS_STRING_BACKSLASH_REPLACEMENTS,
JS_REGEX_REPLACEMENTS,
JS_STRING_NO_BACKSLASH_REPLACEMENTS,
],
(
ContextualizerState::Js(JsState {
tmpl_literal_brace_counts: _,
sub_state: JsSubState::TmplLit(TmplLitState::Regular),
}),
ContextualizerState::PlainText,
) => vec![
JS_STRING_BACKSLASH_REPLACEMENTS,
JS_TEMPLATE_LITERAL_REPLACEMENTS,
JS_STRING_NO_BACKSLASH_REPLACEMENTS,
],
_ => {
return Err(ContextualizationError::IncompatibleTextType {
target_ty: self.to_string(),
source_ty: other.to_string(),
})
}
})
}
fn escaper(&self, input_ty: &str) -> Result<StandardEscaper, ContextualizationError> {
let source = ContextualizerState::try_from(input_ty)
.map_err(|()| ContextualizationError::UnknownTextType(input_ty.to_string()))?;
let empty_strategy = self.empty_strategy();
let multi_line_strategy = self.multi_line_strategy();
Ok(StandardEscaper {
escaper: merge_replacements(
self.escape_stages(&source)?
.into_iter()
.map(|stage| stage.iter().copied()),
),
empty_strategy,
multi_line_strategy,
})
}
}
#[derive(Debug, Default)]
pub struct StandardContextualizer;
impl Contextualizer<StandardEscaper, StandardContextWorker> for StandardContextualizer {
fn default_text_type(&self) -> String {
"text".to_string()
}
fn contextualize(
&self,
text_ty: &str,
) -> Result<StandardContextWorker, ContextualizationError> {
Ok(StandardContextWorker(text_ty.try_into().map_err(|()| {
ContextualizationError::UnknownTextType(text_ty.to_string())
})?))
}
}
#[derive(Debug, Clone)]
pub struct StandardContextWorker(ContextualizerState);
impl ContextWorker<StandardEscaper> for StandardContextWorker {
fn push_static(&mut self, s: &str) -> Result<(), ContextualizationError> {
self.0 = self
.0
.parser()
.parse(s)
.into_result()
.map_err(|_| ContextualizationError::InvalidStatic)?;
Ok(())
}
fn dynamic(&mut self, input_ty: &str) -> Result<StandardEscaper, ContextualizationError> {
self.0.escaper(input_ty)
}
}
#[derive(Debug, Clone)]
struct AhoCorasickEscaper {
searcher: AhoCorasick,
replacements: Vec<String>,
}
impl AhoCorasickEscaper {
fn escape(&self, inp: String) -> String {
let mut iter = self.searcher.find_iter(&inp).peekable();
if iter.peek().is_none() {
return inp;
}
let mut res = String::with_capacity(inp.len());
let mut last_match = 0;
for m in iter {
if !(inp.is_char_boundary(m.start()) && inp.is_char_boundary(m.end())) {
continue;
}
res.push_str(&inp[last_match..m.start()]);
last_match = m.end();
res.push_str(&self.replacements[m.pattern()]);
}
res.push_str(&inp[last_match..]);
res
}
}
#[derive(Debug, Clone)]
pub struct StandardEscaper {
escaper: AhoCorasickEscaper,
empty_strategy: EmptyStrategy,
multi_line_strategy: MultiLineStrategy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum EmptyStrategy {
None,
Deny,
ReplaceRegex,
}
trait HasEmptyStategy {
fn empty_strategy(&self) -> EmptyStrategy;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum MultiLineStrategy {
None,
Condense,
}
trait HasMultiLineStrategy {
fn multi_line_strategy(&self) -> MultiLineStrategy;
}
const FAIL_DENY_EMPTY: &str = "escaping result may not be empty";
impl Escaper for StandardEscaper {
fn escape<'a>(&self, inp: Value) -> Result<Value, String> {
let out = self.escaper.escape(match inp {
Value::Text(inp) => match (self.empty_strategy, inp.is_empty()) {
(_, false) | (EmptyStrategy::None, _) => inp,
(EmptyStrategy::Deny, true) => return Err(FAIL_DENY_EMPTY.to_string()),
(EmptyStrategy::ReplaceRegex, true) => "(?:)".to_string(),
},
Value::Bool(value) => value.to_string(),
Value::Float(value) => value.to_string(),
Value::Int(value) => value.to_string(),
Value::ArrayOrTuple(ref values) => {
if self.multi_line_strategy == MultiLineStrategy::Condense {
return self.escape(inp.clone().try_into().map(Value::Text).unwrap_or(inp));
}
let values: Vec<_> = values
.iter()
.map(|value| self.escape(value.clone()))
.collect::<Result<_, _>>()?;
let is_empty = values
.iter()
.all(|value| matches!(value, Value::Text(s) if s.is_empty()));
match (self.empty_strategy, is_empty) {
(_, false) | (EmptyStrategy::None, _) => (),
(EmptyStrategy::Deny, true) => return Err(FAIL_DENY_EMPTY.to_string()),
(EmptyStrategy::ReplaceRegex, true) => {
return Ok(Value::Text("(?:)".to_string()))
}
}
return Ok(Value::ArrayOrTuple(values));
}
inp @ Value::Struct(_) => return Ok(inp),
});
Ok(Value::Text(out))
}
}