Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/lib/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ pub fn parse_config_string(config_str: &str) -> StyleMatch {
link: parse_style(config.get("link"), default_style.link),
image: parse_style(config.get("image"), default_style.image),
text: parse_style(config.get("text"), default_style.text),
table_header: parse_style(
config.get("table").and_then(|t| t.get("header")),
default_style.table_header,
),
table_cell: parse_style(
config.get("table").and_then(|t| t.get("cell")),
default_style.table_cell,
),
horizontal_rule: parse_style(config.get("horizontal_rule"), default_style.horizontal_rule),
}
}
Expand Down
90 changes: 89 additions & 1 deletion src/lib/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,95 @@ impl Token {
)
}

Token::Table {
headers,
aligns,
rows,
} => {
let mut result = format!("{}{{\n", indent);
result.push_str(&format!("{}\"type\": \"Table\",\n", inner_indent));

// Headers
result.push_str(&format!("{}\"headers\": [\n", inner_indent));
for (i, header_cell) in headers.iter().enumerate() {
result.push_str(&format!("{}[\n", " ".repeat(indent_level + 2)));
for (j, token) in header_cell.iter().enumerate() {
result.push_str(&token.to_readable_json(indent_level + 3));
if j < header_cell.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&format!("{}]", " ".repeat(indent_level + 2)));
if i < headers.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&format!("{}],\n", inner_indent));

// Alignments
result.push_str(&format!("{}\"aligns\": [\n", inner_indent));
for (i, align) in aligns.iter().enumerate() {
let align_str = match align {
genpdfi::Alignment::Left => "Left",
genpdfi::Alignment::Center => "Center",
genpdfi::Alignment::Right => "Right",
};
result.push_str(&format!(
"{}\"{}\"",
" ".repeat(indent_level + 2),
align_str
));
if i < aligns.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&format!("{}],\n", inner_indent));

// Rows
result.push_str(&format!("{}\"rows\": [\n", inner_indent));
for (i, row) in rows.iter().enumerate() {
result.push_str(&format!("{}[\n", " ".repeat(indent_level + 2)));
for (j, cell) in row.iter().enumerate() {
result.push_str(&format!("{}[\n", " ".repeat(indent_level + 3)));
for (k, token) in cell.iter().enumerate() {
result.push_str(&token.to_readable_json(indent_level + 4));
if k < cell.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&format!("{}]", " ".repeat(indent_level + 3)));
if j < row.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&format!("{}]", " ".repeat(indent_level + 2)));
if i < rows.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&format!("{}]\n", inner_indent));
result.push_str(&format!("{}}}", indent));
result
}

Token::TableAlignment(align) => {
let align_str = match align {
genpdfi::Alignment::Left => "Left",
genpdfi::Alignment::Center => "Center",
genpdfi::Alignment::Right => "Right",
};
format!(
"{}{{\n{}\"type\": \"TableAlignment\",\n{}\"alignment\": \"{}\"\n{}}}",
indent, inner_indent, inner_indent, align_str, indent
)
}

Token::HtmlComment(content) => {
format!(
"{}{{\n{}\"type\": \"HtmlComment\",\n{}\"content\": \"{}\"\n{}}}",
Expand All @@ -191,7 +280,6 @@ impl Token {
indent, inner_indent, indent
)
}

Token::Unknown(content) => {
format!(
"{}{{\n{}\"type\": \"Unknown\",\n{}\"content\": \"{}\"\n{}}}",
Expand Down
159 changes: 159 additions & 0 deletions src/lib/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
//! ├── text: String
//! └── url: String

use genpdfi::Alignment;
/// Parsing context — determines which tokens are valid in the current location.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseContext {
Expand Down Expand Up @@ -78,6 +79,14 @@ pub enum Token {
Image(String, String),
/// Plain text content
Text(String),
/// Table with header, alignment info, and rows
Table {
headers: Vec<Vec<Token>>,
aligns: Vec<Alignment>,
rows: Vec<Vec<Vec<Token>>>,
},
/// Text alignment for table columns
TableAlignment(Alignment),
/// HTML comment content
HtmlComment(String),
/// Line break
Expand Down Expand Up @@ -148,6 +157,27 @@ impl Token {
Token::Newline | Token::HorizontalRule => {
// These don't contain text
}
Token::Table {
headers,
aligns: _,
rows,
} => {
for header in headers {
for token in header {
token.collect_text_recursive(result);
}
}
for row in rows {
for cell in row {
for token in cell {
token.collect_text_recursive(result);
}
}
}
}
Token::TableAlignment(_) => {
// These don't contain text
}
}
}
}
Expand Down Expand Up @@ -321,6 +351,13 @@ impl Lexer {
}
'<' if self.is_html_comment_start() => self.parse_html_comment()?,
'\n' => self.parse_newline()?,
'|' if is_line_start => {
if self.is_table_start() {
self.parse_table()?
} else {
self.parse_text(ctx)?
}
}
_ => self.parse_text(ctx)?,
};

Expand Down Expand Up @@ -747,6 +784,95 @@ impl Lexer {
})
}

/// Checks if the current posisiton is the start of a table
fn is_table_start(&self) -> bool {
let rest: String = self.input[self.position..].iter().collect();
// Next line with --- or :---
if let Some(pos) = rest.find('\n') {
let next_line = rest[pos + 1..].lines().next().unwrap_or("");
next_line.contains('-')
} else {
false
}
}

/// Parses a table, handling column alignment
fn parse_table(&mut self) -> Result<Token, LexerError> {
// Parse header row
let header_line = self.read_until_newline();
let header_cells: Vec<String> = header_line
.trim_matches('|')
.split('|')
.map(|s| s.trim().to_string())
.collect();

if self.current_char() == '\n' {
self.advance();
}

// Parse alignment row
let align_line = self.read_until_newline();
let aligns: Vec<Alignment> = align_line
.trim_matches('|')
.split('|')
.map(|s| {
let s = s.trim();
match (s.starts_with(':'), s.ends_with(':')) {
(true, true) => Alignment::Center,
(true, false) => Alignment::Left,
(false, true) => Alignment::Right,
_ => Alignment::Left,
}
})
.collect();

if self.current_char() == '\n' {
self.advance();
}

// Convert header strings to token vectors
let mut headers = Vec::new();
for cell in header_cells {
let mut cell_lexer = Lexer::new(cell);
let parsed = cell_lexer.parse_with_context(ParseContext::TableCell)?;
headers.push(parsed);
}

// Parse rows until blank or non-table start
let mut rows = Vec::new();
while self.position < self.input.len() {
let line = self.read_until_newline();
if line.trim().is_empty() {
break;
}

let cell_texts: Vec<String> = line
.trim_matches('|')
.split('|')
.map(|s| s.trim().to_string())
.collect();

let mut row_tokens = Vec::new();
for cell in cell_texts {
// FIX: large unbreakable words don't fit in cells
let mut cell_lexer = Lexer::new(cell);
let parsed = cell_lexer.parse_with_context(ParseContext::TableCell)?;
row_tokens.push(parsed);
}
rows.push(row_tokens);

if self.current_char() == '\n' {
self.advance();
}
}

Ok(Token::Table {
headers,
aligns,
rows,
})
}

/// Gets the current line's indentation level
fn get_current_indent(&self) -> usize {
let mut count = 0;
Expand Down Expand Up @@ -1201,4 +1327,37 @@ A paragraph with `code` and [link](url).
)]
);
}

#[test]
fn test_tables() {
let input = r#"| Name | Age | City |
|:-----|:---:|----:|
| Alice | 30 | Paris |
| Bob | 25 | Lyon |"#;

let tokens = parse(input);
assert_eq!(
tokens,
vec![Token::Table {
headers: vec![
vec![Token::Text("Name".to_string())],
vec![Token::Text("Age".to_string())],
vec![Token::Text("City".to_string())],
],
aligns: vec![Alignment::Left, Alignment::Center, Alignment::Right],
rows: vec![
vec![
vec![Token::Text("Alice".to_string())],
vec![Token::Text("30".to_string())],
vec![Token::Text("Paris".to_string())],
],
vec![
vec![Token::Text("Bob".to_string())],
vec![Token::Text("25".to_string())],
vec![Token::Text("Lyon".to_string())],
],
],
}]
);
}
}
Loading