use super::entity as entity_mod;
use super::types::TextType;
pub(crate) fn encode_utf8(codepoint: u32, buf: &mut [u8; 4]) -> u8 {
bun_core::strings::encode_wtf8_rune(buf, codepoint) as u8
}
pub use bun_core::strings::eql_case_insensitive_ascii_check_length as ascii_case_eql;
pub fn parse_entity_codepoint(entity_text: &[u8]) -> Option<u32> {
if entity_text.len() < 4 || entity_text[0] != b'&' || entity_text[1] != b'#' {
return None;
}
let mut cp: u32 = 0;
if entity_text[2] == b'x' || entity_text[2] == b'X' {
for &ec in &entity_text[3..] {
if ec == b';' {
break;
}
cp = cp
.wrapping_mul(16)
.wrapping_add(u32::from(bun_core::fmt::hex_digit_value(ec).unwrap_or(0)));
}
} else {
for &ec in &entity_text[2..] {
if ec == b';' {
break;
}
cp = cp.wrapping_mul(10).wrapping_add((ec - b'0') as u32);
}
}
if cp == 0 || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF) {
cp = 0xFFFD;
}
Some(cp)
}
pub fn decode_entity_to_utf8<'a>(entity_text: &[u8], out: &'a mut [u8; 8]) -> Option<&'a [u8]> {
if let Some(cp) = parse_entity_codepoint(entity_text) {
let len = encode_utf8(
cp,
(&mut out[0..4])
.try_into()
.expect("infallible: size matches"),
);
return Some(&out[..len as usize]);
}
if let Some(codepoints) = entity_mod::lookup(entity_text) {
let len1 = encode_utf8(
codepoints[0],
(&mut out[0..4])
.try_into()
.expect("infallible: size matches"),
) as usize;
if codepoints[1] != 0 {
let mut tmp: [u8; 4] = [0; 4];
let len2 = encode_utf8(codepoints[1], &mut tmp) as usize;
out[len1..][..len2].copy_from_slice(&tmp[..len2]);
return Some(&out[..len1 + len2]);
}
return Some(&out[..len1]);
}
None
}
pub fn generate_slug<'a>(
text_buf: &'a mut Vec<u8>,
slug_counts: &mut bun_collections::StringHashMap<u32>,
) -> &'a [u8] {
let text_len = text_buf.len();
let mut out_len: usize = 0;
let mut prev_hyphen: bool = true;
for idx in 0..text_len {
let c = text_buf[idx];
if c >= b'A' && c <= b'Z' {
text_buf[out_len] = c + 32;
out_len += 1;
prev_hyphen = false;
} else if (c >= b'a' && c <= b'z') || (c >= b'0' && c <= b'9') {
text_buf[out_len] = c;
out_len += 1;
prev_hyphen = false;
} else if c == b'-' || c == b' ' {
if !prev_hyphen {
text_buf[out_len] = b'-';
out_len += 1;
prev_hyphen = true;
}
}
}
while out_len > 0 && text_buf[out_len - 1] == b'-' {
out_len -= 1;
}
if let Some(value) = slug_counts.get_mut(&text_buf[..out_len]) {
let count = *value + 1;
*value = count;
text_buf.truncate(out_len);
text_buf.push(b'-');
let mut dec_buf = bun_core::fmt::ItoaBuf::new();
text_buf.extend_from_slice(bun_core::fmt::itoa(&mut dec_buf, count));
return text_buf.as_slice();
}
slug_counts.put_assume_capacity(&text_buf[..out_len], 0);
&text_buf[..out_len]
}
#[derive(Default)]
pub struct HeadingIdTracker {
pub enabled: bool,
pub in_heading: bool,
pub text_buf: Vec<u8>,
pub slug_counts: bun_collections::StringHashMap<u32>,
}
impl HeadingIdTracker {
pub fn init(enabled: bool) -> HeadingIdTracker {
HeadingIdTracker {
enabled,
..Default::default()
}
}
pub fn enter_heading(&mut self) {
if self.enabled {
self.in_heading = true;
}
}
pub fn track_text(&mut self, text_type: TextType, content: &[u8]) {
if !self.in_heading {
return;
}
match text_type {
TextType::NullChar => self.text_buf.extend_from_slice(b"\xEF\xBF\xBD"),
TextType::Br | TextType::Softbr => self.text_buf.extend_from_slice(b" "),
TextType::Html => {}
TextType::Entity => {
let mut buf: [u8; 8] = [0; 8];
match decode_entity_to_utf8(content, &mut buf) {
Some(decoded) => self.text_buf.extend_from_slice(decoded),
None => self.text_buf.extend_from_slice(content),
}
}
_ => self.text_buf.extend_from_slice(content),
}
}
pub fn leave_heading(&mut self) -> Option<&[u8]> {
if !self.enabled {
return None;
}
self.in_heading = false;
Some(generate_slug(&mut self.text_buf, &mut self.slug_counts))
}
pub fn clear_after_heading(&mut self) {
self.text_buf.clear();
}
}