use crate::converter::tier1::tags::TagSpec;
const OUTPUT_CAPACITY_MIN: usize = 1024;
const OUTPUT_CAPACITY_MAX: usize = 256 * 1024;
const OUTPUT_CAPACITY_DIVISOR: usize = 3;
#[derive(Debug, Clone, Default)]
pub struct TableState {
pub rows: Vec<Vec<(String, u16)>>,
pub current_row: Vec<(String, u16)>,
pub current_cell: String,
pub in_thead: bool,
pub in_cell: bool,
pub in_caption: bool,
pub caption_buf: String,
pub caption_text: Option<String>,
pub seen_tbody_close: bool,
pub seen_tfoot: bool,
pub has_th: bool,
pub link_count: usize,
pub first_row_col_count: Option<usize>,
pub inline_mode: bool,
pub had_nested_table: bool,
pub current_cell_colspan: u16,
}
bitflags::bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct EscapeCtx: u8 {
const CODE = 1 << 1;
const PRE = 1 << 2;
const LINK = 1 << 3;
const BLOCKQUOTE = 1 << 4;
const HEADING = 1 << 5;
}
}
#[derive(Debug, Clone)]
pub struct OpenTag {
pub spec: &'static TagSpec,
pub content_start: usize,
pub prev_escape_ctx: EscapeCtx,
pub list_index: u16,
pub ol_start: u16,
pub name_range: std::ops::Range<usize>,
}
const SUMMARY_BUF_CAPACITY: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrapKind {
Summary,
Figcaption,
}
pub struct Tier1State {
pub stack: Vec<OpenTag>,
pub escape_ctx: EscapeCtx,
pub output: String,
pub list_depth: u16,
pub ul_depth: u16,
pub last_block_sep_pos: usize,
pub table_stack: Vec<TableState>,
pub abbr_titles: Vec<Option<String>>,
pub link_stack: Vec<(Option<String>, Option<String>)>,
pub head_range: Option<std::ops::Range<usize>>,
pub pre_lang: Option<String>,
pub summary_buf_stack: Vec<(WrapKind, String)>,
pub canonicalize_attr_entities: bool,
}
impl Tier1State {
#[must_use]
pub fn new(input_len: usize) -> Self {
Self {
stack: Vec::with_capacity(16),
escape_ctx: EscapeCtx::empty(),
output: String::with_capacity(
(input_len / OUTPUT_CAPACITY_DIVISOR).clamp(OUTPUT_CAPACITY_MIN, OUTPUT_CAPACITY_MAX),
),
list_depth: 0,
ul_depth: 0,
last_block_sep_pos: 0,
table_stack: Vec::new(),
link_stack: Vec::new(),
abbr_titles: Vec::new(),
head_range: None,
pre_lang: None,
summary_buf_stack: Vec::new(),
canonicalize_attr_entities: false,
}
}
pub fn cell_or_output_mut(&mut self) -> &mut String {
if let Some((_, buf)) = self.summary_buf_stack.last_mut() {
return buf;
}
if let Some(ts) = self.table_stack.last_mut() {
if ts.in_cell {
return &mut ts.current_cell;
}
if ts.in_caption {
return &mut ts.caption_buf;
}
}
&mut self.output
}
#[must_use]
pub fn in_summary(&self) -> bool {
!self.summary_buf_stack.is_empty()
}
#[must_use]
pub fn summary_at_top(&self) -> bool {
matches!(self.summary_buf_stack.last(), Some((WrapKind::Summary, _)))
}
pub fn push_summary_buf(&mut self, kind: WrapKind) {
self.summary_buf_stack
.push((kind, String::with_capacity(SUMMARY_BUF_CAPACITY)));
}
pub fn pop_summary_buf(&mut self) -> Option<String> {
self.summary_buf_stack.pop().map(|(_, buf)| buf)
}
#[must_use]
pub fn in_table_caption(&self) -> bool {
self.table_stack.last().is_some_and(|ts| ts.in_caption)
}
#[must_use]
pub fn in_table_cell(&self) -> bool {
self.table_stack.last().is_some_and(|ts| ts.in_cell)
}
#[must_use]
pub fn in_any_table_cell(&self) -> bool {
self.table_stack.iter().any(|ts| ts.in_cell)
}
pub fn ensure_blank_line(&mut self) {
let out = &mut self.output;
if out.is_empty() {
return;
}
while out.ends_with(' ') || out.ends_with('\t') {
out.pop();
}
if out.ends_with("\n\n") {
return;
}
if out.ends_with('\n') {
out.push('\n');
} else if out.is_empty() {
} else {
out.push_str("\n\n");
}
}
pub fn ensure_newline(&mut self) {
if !self.output.is_empty() && !self.output.ends_with('\n') {
self.output.push('\n');
}
}
}
pub(crate) fn trim_trailing_horizontal(buf: &mut String) {
while buf.ends_with(' ') || buf.ends_with('\t') {
buf.pop();
}
}