use alloc::string::String;
use alloc::vec::Vec;
use core::fmt::{self, Write as _};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum State {
#[default]
Text,
Tag,
AttrName,
AfterName,
BeforeValue,
HtmlCmt,
Rcdata,
Attr,
Url,
Srcset,
Js,
JsDqStr,
JsSqStr,
JsTmplLit,
JsRegexp,
JsBlockCmt,
JsLineCmt,
JsHtmlOpenCmt,
JsHtmlCloseCmt,
Css,
CssDqStr,
CssSqStr,
CssDqUrl,
CssSqUrl,
CssUrl,
CssBlockCmt,
CssLineCmt,
Error,
MetaContent,
MetaContentUrl,
Dead,
}
impl State {
pub(crate) fn is_comment(self) -> bool {
matches!(
self,
State::HtmlCmt
| State::JsBlockCmt
| State::JsLineCmt
| State::JsHtmlOpenCmt
| State::JsHtmlCloseCmt
| State::CssBlockCmt
| State::CssLineCmt
)
}
pub(crate) fn is_in_tag(self) -> bool {
matches!(
self,
State::Tag | State::AttrName | State::AfterName | State::BeforeValue | State::Attr
)
}
pub(crate) fn is_in_script_literal(self) -> bool {
matches!(
self,
State::JsDqStr | State::JsSqStr | State::JsTmplLit | State::JsRegexp
)
}
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
State::Text => "stateText",
State::Tag => "stateTag",
State::AttrName => "stateAttrName",
State::AfterName => "stateAfterName",
State::BeforeValue => "stateBeforeValue",
State::HtmlCmt => "stateHTMLCmt",
State::Rcdata => "stateRCDATA",
State::Attr => "stateAttr",
State::Url => "stateURL",
State::Srcset => "stateSrcset",
State::Js => "stateJS",
State::JsDqStr => "stateJSDqStr",
State::JsSqStr => "stateJSSqStr",
State::JsTmplLit => "stateJSTmplLit",
State::JsRegexp => "stateJSRegexp",
State::JsBlockCmt => "stateJSBlockCmt",
State::JsLineCmt => "stateJSLineCmt",
State::JsHtmlOpenCmt => "stateJSHTMLOpenCmt",
State::JsHtmlCloseCmt => "stateJSHTMLCloseCmt",
State::Css => "stateCSS",
State::CssDqStr => "stateCSSDqStr",
State::CssSqStr => "stateCSSSqStr",
State::CssDqUrl => "stateCSSDqURL",
State::CssSqUrl => "stateCSSSqURL",
State::CssUrl => "stateCSSURL",
State::CssBlockCmt => "stateCSSBlockCmt",
State::CssLineCmt => "stateCSSLineCmt",
State::Error => "stateError",
State::MetaContent => "stateMetaContent",
State::MetaContentUrl => "stateMetaContentURL",
State::Dead => "stateDead",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum Delim {
#[default]
None,
DoubleQuote,
SingleQuote,
SpaceOrTagEnd,
}
impl fmt::Display for Delim {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Delim::None => "delimNone",
Delim::DoubleQuote => "delimDoubleQuote",
Delim::SingleQuote => "delimSingleQuote",
Delim::SpaceOrTagEnd => "delimSpaceOrTagEnd",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum UrlPart {
#[default]
None,
PreQuery,
QueryOrFrag,
Unknown,
}
impl fmt::Display for UrlPart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
UrlPart::None => "urlPartNone",
UrlPart::PreQuery => "urlPartPreQuery",
UrlPart::QueryOrFrag => "urlPartQueryOrFrag",
UrlPart::Unknown => "urlPartUnknown",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum JsCtx {
#[default]
Regexp,
DivOp,
Unknown,
}
impl fmt::Display for JsCtx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
JsCtx::Regexp => "jsCtxRegexp",
JsCtx::DivOp => "jsCtxDivOp",
JsCtx::Unknown => "jsCtxUnknown",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum Attr {
#[default]
None,
Script,
ScriptType,
Style,
Url,
Srcset,
MetaContent,
}
impl fmt::Display for Attr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Attr::None => "attrNone",
Attr::Script => "attrScript",
Attr::ScriptType => "attrScriptType",
Attr::Style => "attrStyle",
Attr::Url => "attrURL",
Attr::Srcset => "attrSrcset",
Attr::MetaContent => "attrMetaContent",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum Element {
#[default]
None,
Script,
Style,
Textarea,
Title,
Meta,
}
impl fmt::Display for Element {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Element::None => "elementNone",
Element::Script => "elementScript",
Element::Style => "elementStyle",
Element::Textarea => "elementTextarea",
Element::Title => "elementTitle",
Element::Meta => "elementMeta",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct Context {
pub(crate) state: State,
pub(crate) delim: Delim,
pub(crate) url_part: UrlPart,
pub(crate) js_ctx: JsCtx,
pub(crate) js_brace_depth: Vec<i32>,
pub(crate) attr: Attr,
pub(crate) element: Element,
}
pub(crate) fn write_int_slice(out: &mut String, v: &[i32]) {
out.push('[');
for (i, d) in v.iter().enumerate() {
if i != 0 {
out.push(' ');
}
let _ = write!(out, "{d}");
}
out.push(']');
}
impl Context {
pub(crate) fn text() -> Context {
Context::default()
}
pub(crate) fn in_state(state: State) -> Context {
Context {
state,
..Context::default()
}
}
pub(crate) fn error() -> Context {
Context::in_state(State::Error)
}
pub(crate) fn mangle(&self, template_name: &str) -> String {
if self.state == State::Text {
return String::from(template_name);
}
let mut s = String::from(template_name);
let _ = write!(s, "$htmltemplate_{}", self.state);
if self.delim != Delim::None {
let _ = write!(s, "_{}", self.delim);
}
if self.url_part != UrlPart::None {
let _ = write!(s, "_{}", self.url_part);
}
if self.js_ctx != JsCtx::Regexp {
let _ = write!(s, "_{}", self.js_ctx);
}
if !self.js_brace_depth.is_empty() {
s.push_str("_jsBraceDepth(");
write_int_slice(&mut s, &self.js_brace_depth);
s.push(')');
}
if self.attr != Attr::None {
let _ = write!(s, "_{}", self.attr);
}
if self.element != Element::None {
let _ = write!(s, "_{}", self.element);
}
s
}
pub(crate) fn nudge(mut self) -> Context {
match self.state {
State::Tag => self.state = State::AttrName,
State::BeforeValue => {
self.state = attr_start_state(self.attr);
self.delim = Delim::SpaceOrTagEnd;
self.attr = Attr::None;
}
State::AfterName => {
self.state = State::AttrName;
self.attr = Attr::None;
}
_ => {}
}
self
}
pub(crate) fn join(a: Context, b: Context) -> Option<Context> {
if a.state == State::Error {
return None;
}
if b.state == State::Error {
return None;
}
if a.state == State::Dead {
return Some(b);
}
if b.state == State::Dead {
return Some(a);
}
if a == b {
return Some(a);
}
let mut c = a.clone();
c.url_part = b.url_part;
if c == b {
c.url_part = UrlPart::Unknown;
return Some(c);
}
let mut c = a.clone();
c.js_ctx = b.js_ctx;
if c == b {
c.js_ctx = JsCtx::Unknown;
return Some(c);
}
let na = a.clone().nudge();
let nb = b.clone().nudge();
if !(na == a && nb == b)
&& let Some(e) = Context::join(na, nb)
{
return Some(e);
}
None
}
pub(crate) fn join_range(
mut c0: Context,
breaks: &[Context],
continues: &[Context],
) -> Option<Context> {
for c in breaks {
c0 = Context::join(c0, c.clone())?;
}
for c in continues {
c0 = Context::join(c0, c.clone())?;
}
Some(c0)
}
}
pub(crate) fn attr_start_state(a: Attr) -> State {
match a {
Attr::None => State::Attr,
Attr::Script => State::Js,
Attr::ScriptType => State::Attr,
Attr::Style => State::Css,
Attr::Url => State::Url,
Attr::Srcset => State::Srcset,
Attr::MetaContent => State::MetaContent,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AttrContentType {
Plain,
Css,
Html,
Js,
Url,
Srcset,
Unsafe,
}
pub(crate) fn attr_type(name: &str) -> AttrContentType {
let name = if let Some(stripped) = name.strip_prefix("data-") {
stripped
} else if let Some(colon) = name.find(':') {
if &name[..colon] == "xmlns" {
return AttrContentType::Url;
}
&name[colon + 1..]
} else {
name
};
if let Some(t) = attr_type_map(name) {
return t;
}
if name.starts_with("on") {
return AttrContentType::Js;
}
if name.contains("src") || name.contains("uri") || name.contains("url") {
return AttrContentType::Url;
}
AttrContentType::Plain
}
fn attr_type_map(name: &str) -> Option<AttrContentType> {
use AttrContentType::{Css, Html, Plain, Srcset, Unsafe, Url};
Some(match name {
"accept" => Plain,
"accept-charset" => Unsafe,
"action" => Url,
"alt" => Plain,
"archive" => Url,
"async" => Unsafe,
"autocomplete" => Plain,
"autofocus" => Plain,
"autoplay" => Plain,
"background" => Url,
"border" => Plain,
"checked" => Plain,
"cite" => Url,
"challenge" => Unsafe,
"charset" => Unsafe,
"class" => Plain,
"classid" => Url,
"codebase" => Url,
"cols" => Plain,
"colspan" => Plain,
"content" => Unsafe,
"contenteditable" => Plain,
"contextmenu" => Plain,
"controls" => Plain,
"coords" => Plain,
"crossorigin" => Unsafe,
"data" => Url,
"datetime" => Plain,
"default" => Plain,
"defer" => Unsafe,
"dir" => Plain,
"dirname" => Plain,
"disabled" => Plain,
"draggable" => Plain,
"dropzone" => Plain,
"enctype" => Unsafe,
"for" => Plain,
"form" => Unsafe,
"formaction" => Url,
"formenctype" => Unsafe,
"formmethod" => Unsafe,
"formnovalidate" => Unsafe,
"formtarget" => Plain,
"headers" => Plain,
"height" => Plain,
"hidden" => Plain,
"high" => Plain,
"href" => Url,
"hreflang" => Plain,
"http-equiv" => Unsafe,
"icon" => Url,
"id" => Plain,
"ismap" => Plain,
"keytype" => Unsafe,
"kind" => Plain,
"label" => Plain,
"lang" => Plain,
"language" => Unsafe,
"list" => Plain,
"longdesc" => Url,
"loop" => Plain,
"low" => Plain,
"manifest" => Url,
"max" => Plain,
"maxlength" => Plain,
"media" => Plain,
"mediagroup" => Plain,
"method" => Unsafe,
"min" => Plain,
"multiple" => Plain,
"name" => Plain,
"novalidate" => Unsafe,
"open" => Plain,
"optimum" => Plain,
"pattern" => Unsafe,
"placeholder" => Plain,
"poster" => Url,
"profile" => Url,
"preload" => Plain,
"pubdate" => Plain,
"radiogroup" => Plain,
"readonly" => Plain,
"rel" => Unsafe,
"required" => Plain,
"reversed" => Plain,
"rows" => Plain,
"rowspan" => Plain,
"sandbox" => Unsafe,
"spellcheck" => Plain,
"scope" => Plain,
"scoped" => Plain,
"seamless" => Plain,
"selected" => Plain,
"shape" => Plain,
"size" => Plain,
"sizes" => Plain,
"span" => Plain,
"src" => Url,
"srcdoc" => Html,
"srclang" => Plain,
"srcset" => Srcset,
"start" => Plain,
"step" => Plain,
"style" => Css,
"tabindex" => Plain,
"target" => Plain,
"title" => Plain,
"type" => Unsafe,
"usemap" => Url,
"value" => Unsafe,
"width" => Plain,
"wrap" => Plain,
"xmlns" => Url,
_ => return None,
})
}