use std::fmt;
use crate::dimension::is_valid_dimension_unit;
use texform_interface::column::{
ArrayPadding, ColumnAlign, ColumnSpec, FrameLine, FrameSide, LineStyle, RowAlign, VerticalAlign,
};
const MAX_COLUMNS: usize = 10000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ColumnParseError {
MaxColumns,
BadPreamToken(char),
MissingColumnDimOrUnits(char),
MissingArgForColumn(char),
MissingCloseBrace,
ColArgNotNum,
}
impl fmt::Display for ColumnParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ColumnParseError::MaxColumns => {
write!(
f,
"Too many column specifiers (perhaps looping column definitions?)"
)
}
ColumnParseError::BadPreamToken(c) => write!(f, "Illegal pream-token ({c})"),
ColumnParseError::MissingColumnDimOrUnits(c) => write!(
f,
"Missing dimension or its units for {c} column declaration"
),
ColumnParseError::MissingArgForColumn(c) => {
write!(f, "Missing argument for {c} column declaration")
}
ColumnParseError::MissingCloseBrace => write!(f, "Missing close brace"),
ColumnParseError::ColArgNotNum => {
write!(f, "First argument to * column specifier must be a number")
}
}
}
}
pub fn parse_column_template(template: &str) -> Result<ColumnSpec, ColumnParseError> {
let mut state = ColumnState::new(template);
let mut n = 0usize;
while state.cursor < state.template.len() {
if n > MAX_COLUMNS {
return Err(ColumnParseError::MaxColumns);
}
n += 1;
let current_char = state
.next_char()
.ok_or(ColumnParseError::MissingCloseBrace)?;
state.current_char = current_char;
handle_column_char(current_char, &mut state)?;
}
let mut spec = ColumnSpec::new(template.to_string(), state.template.clone());
set_column_aligns(&state, &mut spec);
set_column_widths(&state, &mut spec);
set_column_spacing(&state, &mut spec);
set_column_lines(&state, &mut spec);
set_padding(&state, &mut spec);
set_column_extras(&state, &mut spec);
Ok(spec)
}
#[derive(Clone)]
struct ColumnState {
template: String,
cursor: usize,
current_char: char,
column_index: usize,
column_aligns: Vec<Option<ColumnAlign>>,
column_widths: Vec<Option<String>>,
column_spacing: Vec<Option<String>>,
column_lines: Vec<Option<LineStyle>>,
column_starts: Vec<Option<String>>,
column_ends: Vec<Option<String>>,
column_extras: Vec<bool>,
row_aligns: Vec<Option<RowAlign>>,
}
impl ColumnState {
fn new(template: &str) -> Self {
ColumnState {
template: template.to_string(),
cursor: 0,
current_char: '\0',
column_index: 0,
column_aligns: Vec::new(),
column_widths: Vec::new(),
column_spacing: Vec::new(),
column_lines: Vec::new(),
column_starts: Vec::new(),
column_ends: Vec::new(),
column_extras: Vec::new(),
row_aligns: Vec::new(),
}
}
fn next_char(&mut self) -> Option<char> {
let rest = self.template.get(self.cursor..)?;
let mut chars = rest.chars();
let current_char = chars.next()?;
self.cursor += current_char.len_utf8();
Some(current_char)
}
fn peek_char(&self) -> Option<char> {
self.template.get(self.cursor..)?.chars().next()
}
}
fn handle_column_char(c: char, state: &mut ColumnState) -> Result<(), ColumnParseError> {
match c {
'l' => {
set_column_align(state, state.column_index, ColumnAlign::Left);
state.column_index += 1;
Ok(())
}
'c' => {
set_column_align(state, state.column_index, ColumnAlign::Center);
state.column_index += 1;
Ok(())
}
'r' => {
set_column_align(state, state.column_index, ColumnAlign::Right);
state.column_index += 1;
Ok(())
}
'p' => get_column(state, VerticalAlign::Top, Some(ColumnAlign::Left)),
'm' => get_column(state, VerticalAlign::Middle, Some(ColumnAlign::Left)),
'b' => get_column(state, VerticalAlign::Bottom, Some(ColumnAlign::Left)),
'w' | 'W' => get_column(state, VerticalAlign::Top, None),
'|' => {
add_rule(state, LineStyle::Solid);
Ok(())
}
':' => {
add_rule(state, LineStyle::Dashed);
Ok(())
}
'>' => {
let value = get_braces(state)?;
append_column_start(state, state.column_index, &value);
Ok(())
}
'<' => {
let idx = state.column_index.saturating_sub(1);
let value = get_braces(state)?;
append_column_end(state, idx, &value);
Ok(())
}
'@' => {
let value = get_braces(state)?;
add_at(state, value);
Ok(())
}
'!' => {
let value = get_braces(state)?;
add_bang(state, value);
Ok(())
}
'*' => repeat(state),
'P' => macro_column(state, ">{$}p{#1}<{$}", 1),
'M' => macro_column(state, ">{$}m{#1}<{$}", 1),
'B' => macro_column(state, ">{$}b{#1}<{$}", 1),
' ' => Ok(()),
_ => Err(ColumnParseError::BadPreamToken(c)),
}
}
fn get_column(
state: &mut ColumnState,
vertical: VerticalAlign,
default_align: Option<ColumnAlign>,
) -> Result<(), ColumnParseError> {
let align = if let Some(align) = default_align {
align
} else {
get_align(state)?
};
let width = get_dimen(state)?;
set_column_align(state, state.column_index, align);
set_option_string(&mut state.column_widths, state.column_index, width.clone());
set_option(
&mut state.row_aligns,
state.column_index,
RowAlign {
vertical,
width,
align,
},
);
state.column_index += 1;
Ok(())
}
fn get_dimen(state: &mut ColumnState) -> Result<String, ColumnParseError> {
let dim = get_braces(state)?;
if !is_valid_dimension(&dim) {
return Err(ColumnParseError::MissingColumnDimOrUnits(
state.current_char,
));
}
Ok(dim)
}
fn get_align(state: &mut ColumnState) -> Result<ColumnAlign, ColumnParseError> {
let align = get_braces(state)?;
let lowered = align.to_lowercase();
Ok(match lowered.as_str() {
"l" => ColumnAlign::Left,
"c" => ColumnAlign::Center,
"r" => ColumnAlign::Right,
_ => ColumnAlign::Empty,
})
}
fn get_braces(state: &mut ColumnState) -> Result<String, ColumnParseError> {
while matches!(state.peek_char(), Some(' ')) {
state.next_char();
}
if state.cursor >= state.template.len() {
return Err(ColumnParseError::MissingArgForColumn(state.current_char));
}
if state.peek_char() != Some('{') {
return Ok(state.next_char().unwrap().to_string());
}
state.next_char(); let start = state.cursor;
let mut braces = 1usize;
while state.cursor < state.template.len() {
let ch = state.next_char().unwrap();
match ch {
'\\' => {
if state.cursor < state.template.len() {
state.next_char();
}
}
'{' => braces += 1,
'}' => {
braces -= 1;
if braces == 0 {
let end = state.cursor - 1; return Ok(state.template[start..end].to_string());
}
}
_ => {}
}
}
Err(ColumnParseError::MissingCloseBrace)
}
fn macro_column(
state: &mut ColumnState,
macro_template: &str,
n: usize,
) -> Result<(), ColumnParseError> {
let mut args = Vec::with_capacity(n);
for _ in 0..n {
args.push(get_braces(state)?);
}
let expansion = substitute_args(&args, macro_template)?;
let rest = state.template[state.cursor..].to_string();
state.template = format!("{expansion}{rest}");
state.cursor = 0;
Ok(())
}
fn add_rule(state: &mut ColumnState, style: LineStyle) {
if get_option(&state.column_lines, state.column_index).is_some() {
add_at(state, r"\,".to_string());
}
set_option(&mut state.column_lines, state.column_index, style);
if get_option(&state.column_spacing, state.column_index).as_deref() == Some("0") {
set_option_string(
&mut state.column_starts,
state.column_index,
r"\hspace{.5em}".to_string(),
);
}
}
fn add_at(state: &mut ColumnState, macro_text: String) {
let column_index = state.column_index;
set_column_extra(state, column_index, true);
set_column_align(state, column_index, ColumnAlign::Center);
if get_option(&state.column_lines, column_index).is_some() {
if get_option(&state.column_spacing, column_index).as_deref() == Some(".5em") {
if column_index > 0 {
append_column_start(state, column_index - 1, r"\hspace{.25em}");
}
} else if get_option(&state.column_spacing, column_index).is_none() && column_index > 0 {
append_column_end(state, column_index - 1, r"\hspace{.5em}");
}
}
set_option_string(&mut state.column_starts, column_index, macro_text);
set_option_string(&mut state.column_spacing, column_index, "0".to_string());
state.column_index += 1;
set_option_string(
&mut state.column_spacing,
state.column_index,
"0".to_string(),
);
}
fn add_bang(state: &mut ColumnState, macro_text: String) {
let column_index = state.column_index;
set_column_extra(state, column_index, true);
set_column_align(state, column_index, ColumnAlign::Center);
let prefix = if get_option(&state.column_spacing, column_index).as_deref() == Some("0")
&& get_option(&state.column_lines, column_index).is_some()
{
r"\hspace{.25em}"
} else {
""
};
set_option_string(
&mut state.column_starts,
column_index,
format!("{prefix}{macro_text}"),
);
if get_option(&state.column_spacing, column_index).is_none() {
set_option_string(&mut state.column_spacing, column_index, ".5em".to_string());
}
state.column_index += 1;
set_option_string(
&mut state.column_spacing,
state.column_index,
".5em".to_string(),
);
}
fn repeat(state: &mut ColumnState) -> Result<(), ColumnParseError> {
let num = get_braces(state)?;
let cols = get_braces(state)?;
let parsed = num.parse::<isize>().ok();
if parsed.is_none() || parsed.unwrap() < 0 || parsed.unwrap().to_string() != num {
return Err(ColumnParseError::ColArgNotNum);
}
let n = parsed.unwrap() as usize;
let rest = state.template[state.cursor..].to_string();
state.template = format!("{}{}", cols.repeat(n), rest);
state.cursor = 0;
Ok(())
}
fn substitute_args(args: &[String], text: &str) -> Result<String, ColumnParseError> {
let mut out = String::new();
let chars: Vec<char> = text.chars().collect();
let mut cursor = 0usize;
while cursor < chars.len() {
let current_char = chars[cursor];
if current_char == '\\' {
out.push(current_char);
cursor += 1;
if cursor < chars.len() {
out.push(chars[cursor]);
cursor += 1;
}
continue;
}
if current_char == '#' {
cursor += 1;
if cursor >= chars.len() {
return Err(ColumnParseError::ColArgNotNum);
}
let marker = chars[cursor];
if marker == '#' {
out.push('#');
cursor += 1;
continue;
}
if !marker.is_ascii_digit() || marker == '0' {
return Err(ColumnParseError::ColArgNotNum);
}
let idx = (marker as u8 - b'1') as usize;
if idx >= args.len() {
return Err(ColumnParseError::ColArgNotNum);
}
out.push_str(&args[idx]);
cursor += 1;
continue;
}
out.push(current_char);
cursor += 1;
}
Ok(out)
}
fn set_column_aligns(state: &ColumnState, spec: &mut ColumnSpec) {
spec.column_align = state
.column_aligns
.iter()
.map(|a| a.unwrap_or(ColumnAlign::Center))
.collect();
}
fn set_column_widths(state: &ColumnState, spec: &mut ColumnSpec) {
if !state.column_widths.iter().any(|w| w.is_some()) {
return;
}
let mut widths = state.column_widths.clone();
if widths.len() < state.column_aligns.len() {
widths.push(Some("auto".to_string()));
}
spec.column_width = widths
.into_iter()
.map(|w| w.unwrap_or_else(|| "auto".to_string()))
.collect();
}
fn set_column_spacing(state: &ColumnState, spec: &mut ColumnSpec) {
if !state.column_spacing.iter().any(|s| s.is_some()) {
return;
}
let mut spacing = state.column_spacing.clone();
if spacing.len() < state.column_aligns.len() {
spacing.push(Some("1em".to_string()));
}
spec.column_spacing = spacing
.into_iter()
.skip(1)
.map(|s| s.unwrap_or_else(|| "1em".to_string()))
.collect();
}
fn set_column_lines(state: &ColumnState, spec: &mut ColumnSpec) {
if !state.column_lines.iter().any(|l| l.is_some()) {
return;
}
let mut lines = state.column_lines.clone();
if let Some(Some(style)) = lines.first().copied() {
spec.frame.push(FrameLine {
side: FrameSide::Left,
style,
});
}
if lines.len() > state.column_aligns.len() {
if let Some(Some(style)) = lines.pop() {
spec.frame.push(FrameLine {
side: FrameSide::Right,
style,
});
}
} else if lines.len() < state.column_aligns.len() {
lines.push(Some(LineStyle::None));
}
if lines.len() > 1 {
spec.column_lines = lines
.into_iter()
.skip(1)
.map(|l| l.unwrap_or(LineStyle::None))
.collect();
}
}
fn set_padding(state: &ColumnState, spec: &mut ColumnSpec) {
if state.column_aligns.is_empty() {
return;
}
let left_extra = state.column_extras.first().copied().unwrap_or(false);
let last_column_index = state.column_aligns.len() - 1;
let right_extra = state
.column_extras
.get(last_column_index)
.copied()
.unwrap_or(false);
if !left_extra && !right_extra {
return;
}
let left = get_option(&state.column_spacing, 0).unwrap_or_else(|| ".5em".to_string());
let right = if right_extra {
get_option(&state.column_spacing, last_column_index).unwrap_or_else(|| ".5em".to_string())
} else {
".5em".to_string()
};
spec.array_padding = Some(ArrayPadding { left, right });
}
fn set_column_extras(state: &ColumnState, spec: &mut ColumnSpec) {
let n = [
state.column_aligns.len(),
state.column_starts.len(),
state.column_ends.len(),
state.column_extras.len(),
state.row_aligns.len(),
]
.into_iter()
.max()
.unwrap_or(0);
spec.column_start = (0..n)
.map(|index| get_option(&state.column_starts, index).unwrap_or_default())
.collect();
spec.column_end = (0..n)
.map(|index| get_option(&state.column_ends, index).unwrap_or_default())
.collect();
spec.column_extra = (0..n)
.map(|index| state.column_extras.get(index).copied().unwrap_or(false))
.collect();
spec.row_align = (0..n)
.map(|index| get_option(&state.row_aligns, index))
.collect();
}
fn is_valid_dimension(raw: &str) -> bool {
let s = raw.trim();
if s.is_empty() {
return false;
}
let chars: Vec<char> = s.chars().collect();
let mut cursor = 0usize;
if matches!(chars.get(cursor), Some('+') | Some('-')) {
cursor += 1;
}
let mut int_digits = 0usize;
while matches!(chars.get(cursor), Some(ch) if ch.is_ascii_digit()) {
cursor += 1;
int_digits += 1;
}
let mut frac_digits = 0usize;
if matches!(chars.get(cursor), Some('.') | Some(',')) {
cursor += 1;
while matches!(chars.get(cursor), Some(ch) if ch.is_ascii_digit()) {
cursor += 1;
frac_digits += 1;
}
}
if int_digits == 0 && frac_digits == 0 {
return false;
}
while matches!(chars.get(cursor), Some(ch) if ch.is_whitespace()) {
cursor += 1;
}
let unit_start = cursor;
while matches!(chars.get(cursor), Some(ch) if ch.is_ascii_alphabetic()) {
cursor += 1;
}
if unit_start == cursor {
return false;
}
let unit: String = chars[unit_start..cursor].iter().collect();
if !is_valid_dimension_unit(&unit) {
return false;
}
while matches!(chars.get(cursor), Some(ch) if ch.is_whitespace()) {
cursor += 1;
}
cursor == chars.len()
}
fn set_option<T: Clone>(vec: &mut Vec<Option<T>>, index: usize, value: T) {
if vec.len() <= index {
vec.resize(index + 1, None);
}
vec[index] = Some(value);
}
fn set_option_string(vec: &mut Vec<Option<String>>, index: usize, value: String) {
set_option(vec, index, value);
}
fn set_column_align(state: &mut ColumnState, index: usize, value: ColumnAlign) {
set_option(&mut state.column_aligns, index, value);
}
fn set_column_extra(state: &mut ColumnState, index: usize, value: bool) {
if state.column_extras.len() <= index {
state.column_extras.resize(index + 1, false);
}
state.column_extras[index] = value;
}
fn get_option<T: Clone>(vec: &[Option<T>], index: usize) -> Option<T> {
vec.get(index).and_then(|v| v.clone())
}
fn append_column_start(state: &mut ColumnState, index: usize, value: &str) {
let mut cur = get_option(&state.column_starts, index).unwrap_or_default();
cur.push_str(value);
set_option_string(&mut state.column_starts, index, cur);
}
fn append_column_end(state: &mut ColumnState, index: usize, value: &str) {
let mut cur = get_option(&state.column_ends, index).unwrap_or_default();
cur.push_str(value);
set_option_string(&mut state.column_ends, index, cur);
}