Feat: continue refactoring

This commit is contained in:
2026-02-01 13:39:25 +01:00
parent c356aebfde
commit dd77f6d92d
20 changed files with 766 additions and 581 deletions

View File

@@ -1,6 +1,6 @@
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
use cagire_markdown::{CodeHighlighter, MarkdownTheme};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line as RLine, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap};
use ratatui::Frame;
@@ -11,6 +11,78 @@ use crate::state::HelpFocus;
use crate::theme;
use crate::views::highlight;
struct AppTheme;
impl MarkdownTheme for AppTheme {
fn h1(&self) -> Style {
Style::new()
.fg(theme::get().markdown.h1)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
}
fn h2(&self) -> Style {
Style::new()
.fg(theme::get().markdown.h2)
.add_modifier(Modifier::BOLD)
}
fn h3(&self) -> Style {
Style::new()
.fg(theme::get().markdown.h3)
.add_modifier(Modifier::BOLD)
}
fn text(&self) -> Style {
Style::new().fg(theme::get().markdown.text)
}
fn code(&self) -> Style {
Style::new().fg(theme::get().markdown.code)
}
fn code_border(&self) -> Style {
Style::new().fg(theme::get().markdown.code_border)
}
fn link(&self) -> Style {
Style::new()
.fg(theme::get().markdown.link)
.add_modifier(Modifier::UNDERLINED)
}
fn link_url(&self) -> Style {
Style::new().fg(theme::get().markdown.link_url)
}
fn quote(&self) -> Style {
Style::new().fg(theme::get().markdown.quote)
}
fn list(&self) -> Style {
Style::new().fg(theme::get().markdown.list)
}
fn table_header_bg(&self) -> Color {
theme::get().ui.surface
}
fn table_row_even(&self) -> Color {
theme::get().table.row_even
}
fn table_row_odd(&self) -> Color {
theme::get().table.row_odd
}
}
struct ForthHighlighter;
impl CodeHighlighter for ForthHighlighter {
fn highlight(&self, line: &str) -> Vec<(Style, String)> {
highlight::highlight_line(line)
}
}
enum DocEntry {
Section(&'static str),
Topic(&'static str, &'static str),
@@ -202,7 +274,7 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let has_query = !query.is_empty();
let query_lower = query.to_lowercase();
let lines = parse_markdown(md);
let lines = cagire_markdown::parse(md, &AppTheme, &ForthHighlighter);
let has_search_bar = app.ui.help_search_active || has_query;
let content_area = if has_search_bar {
@@ -333,285 +405,3 @@ pub fn find_match(query: &str) -> Option<(usize, usize)> {
}
None
}
fn code_border_style() -> Style {
let theme = theme::get();
Style::new().fg(theme.markdown.code_border)
}
fn preprocess_markdown(md: &str) -> String {
let mut out = String::with_capacity(md.len());
for line in md.lines() {
// Convert dash list markers to asterisks (minimad only recognizes *)
let line = convert_dash_lists(line);
let mut result = String::with_capacity(line.len());
let mut chars = line.char_indices().peekable();
let bytes = line.as_bytes();
while let Some((i, c)) = chars.next() {
if c == '`' {
result.push(c);
for (_, ch) in chars.by_ref() {
result.push(ch);
if ch == '`' {
break;
}
}
continue;
}
if c == '_' {
let before_is_space = i == 0 || bytes[i - 1] == b' ';
if before_is_space {
if let Some(end) = line[i + 1..].find('_') {
let inner = &line[i + 1..i + 1 + end];
if !inner.is_empty() {
result.push('*');
result.push_str(inner);
result.push('*');
for _ in 0..end {
chars.next();
}
chars.next(); // skip closing _
continue;
}
}
}
}
result.push(c);
}
out.push_str(&result);
out.push('\n');
}
out
}
fn convert_dash_lists(line: &str) -> String {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("- ") {
let indent = line.len() - trimmed.len();
format!("{}* {}", " ".repeat(indent), rest)
} else {
line.to_string()
}
}
fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
let processed = preprocess_markdown(md);
let text = minimad::Text::from(processed.as_str());
let mut lines = Vec::new();
let mut code_line_nr: usize = 0;
let mut table_buffer: Vec<TableRow> = Vec::new();
let flush_table = |buf: &mut Vec<TableRow>, out: &mut Vec<RLine<'static>>| {
if buf.is_empty() {
return;
}
let col_widths = compute_column_widths(buf);
for (row_idx, row) in buf.drain(..).enumerate() {
out.push(render_table_row(row, row_idx, &col_widths));
}
};
for line in text.lines {
match line {
Line::Normal(composite) if composite.style == CompositeStyle::Code => {
flush_table(&mut table_buffer, &mut lines);
code_line_nr += 1;
let raw: String = composite
.compounds
.iter()
.map(|c: &minimad::Compound| c.src)
.collect();
let mut spans = vec![
Span::styled(format!(" {code_line_nr:>2} "), code_border_style()),
Span::styled("", code_border_style()),
];
spans.extend(
highlight::highlight_line(&raw)
.into_iter()
.map(|(style, text)| Span::styled(text, style)),
);
lines.push(RLine::from(spans));
}
Line::Normal(composite) => {
flush_table(&mut table_buffer, &mut lines);
code_line_nr = 0;
lines.push(composite_to_line(composite));
}
Line::TableRow(row) => {
code_line_nr = 0;
table_buffer.push(row);
}
Line::TableRule(_) => {
// Skip the separator line (---|---|---)
}
_ => {
flush_table(&mut table_buffer, &mut lines);
code_line_nr = 0;
lines.push(RLine::from(""));
}
}
}
flush_table(&mut table_buffer, &mut lines);
lines
}
fn cell_text_width(cell: &Composite) -> usize {
cell.compounds.iter().map(|c| c.src.chars().count()).sum()
}
fn compute_column_widths(rows: &[TableRow]) -> Vec<usize> {
let mut widths: Vec<usize> = Vec::new();
for row in rows {
for (i, cell) in row.cells.iter().enumerate() {
let w = cell_text_width(cell);
if i >= widths.len() {
widths.push(w);
} else if w > widths[i] {
widths[i] = w;
}
}
}
widths
}
fn render_table_row(row: TableRow, row_idx: usize, col_widths: &[usize]) -> RLine<'static> {
let theme = theme::get();
let is_header = row_idx == 0;
let bg = if is_header {
theme.ui.surface
} else if row_idx.is_multiple_of(2) {
theme.table.row_even
} else {
theme.table.row_odd
};
let base_style = if is_header {
Style::new()
.fg(theme.markdown.text)
.bg(bg)
.add_modifier(Modifier::BOLD)
} else {
Style::new().fg(theme.markdown.text).bg(bg)
};
let sep_style = Style::new().fg(theme.markdown.code_border).bg(bg);
let mut spans: Vec<Span<'static>> = Vec::new();
for (i, cell) in row.cells.into_iter().enumerate() {
if i > 0 {
spans.push(Span::styled("", sep_style));
}
let target_width = col_widths.get(i).copied().unwrap_or(0);
let cell_width = cell
.compounds
.iter()
.map(|c| c.src.chars().count())
.sum::<usize>();
for compound in cell.compounds {
compound_to_spans(compound, base_style, &mut spans);
}
let padding = target_width.saturating_sub(cell_width);
if padding > 0 {
spans.push(Span::styled(" ".repeat(padding), base_style));
}
}
RLine::from(spans)
}
fn composite_to_line(composite: Composite) -> RLine<'static> {
let theme = theme::get();
let base_style = match composite.style {
CompositeStyle::Header(1) => Style::new()
.fg(theme.markdown.h1)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
CompositeStyle::Header(2) => Style::new()
.fg(theme.markdown.h2)
.add_modifier(Modifier::BOLD),
CompositeStyle::Header(_) => Style::new()
.fg(theme.markdown.h3)
.add_modifier(Modifier::BOLD),
CompositeStyle::ListItem(_) => Style::new().fg(theme.markdown.list),
CompositeStyle::Quote => Style::new().fg(theme.markdown.quote),
CompositeStyle::Code => Style::new().fg(theme.markdown.code),
CompositeStyle::Paragraph => Style::new().fg(theme.markdown.text),
};
let prefix: String = match composite.style {
CompositeStyle::ListItem(depth) => {
let indent = " ".repeat(depth as usize);
format!("{indent}")
}
CompositeStyle::Quote => "".to_string(),
_ => String::new(),
};
let mut spans: Vec<Span<'static>> = Vec::new();
if !prefix.is_empty() {
spans.push(Span::styled(prefix, base_style));
}
for compound in composite.compounds {
compound_to_spans(compound, base_style, &mut spans);
}
RLine::from(spans)
}
fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec<Span<'static>>) {
let theme = theme::get();
let mut style = base;
if compound.bold {
style = style.add_modifier(Modifier::BOLD);
}
if compound.italic {
style = style.add_modifier(Modifier::ITALIC);
}
if compound.code {
style = Style::new().fg(theme.markdown.code);
}
if compound.strikeout {
style = style.add_modifier(Modifier::CROSSED_OUT);
}
let src = compound.src.to_string();
let link_style = Style::new()
.fg(theme.markdown.link)
.add_modifier(Modifier::UNDERLINED);
let mut rest = src.as_str();
while let Some(start) = rest.find('[') {
let after_bracket = &rest[start + 1..];
if let Some(text_end) = after_bracket.find("](") {
let url_start = start + 1 + text_end + 2;
if let Some(url_end) = rest[url_start..].find(')') {
if start > 0 {
out.push(Span::styled(rest[..start].to_string(), style));
}
let text = &rest[start + 1..start + 1 + text_end];
let url = &rest[url_start..url_start + url_end];
if text == url {
out.push(Span::styled(url.to_string(), link_style));
} else {
out.push(Span::styled(text.to_string(), link_style));
out.push(Span::styled(
format!(" ({url})"),
Style::new().fg(theme.markdown.link_url),
));
}
rest = &rest[url_start + url_end + 1..];
continue;
}
}
out.push(Span::styled(rest[..start + 1].to_string(), style));
rest = &rest[start + 1..];
}
if !rest.is_empty() {
out.push(Span::styled(rest.to_string(), style));
}
}