fmtview 0.3.1

Fast CLI viewer for highlighting, search, and diffs across JSON, JSONL, markup, Markdown, TOML, text, and Jinja
Documentation
use ratatui::text::Span;

use super::{
    checkpoints::HighlightCheckpointIndex,
    util::{highlight_string_segment_window, push_span_window, take_while},
};
use crate::viewer::palette::{
    bool_style, key_style, number_style, plain_style, punctuation_style, string_style,
};

pub(crate) fn highlight_toml_line_window(
    line: &str,
    window_start: usize,
    window_end: usize,
    _index: Option<&mut HighlightCheckpointIndex>,
) -> Vec<Span<'static>> {
    let mut spans = Vec::new();
    let comment_start = comment_start(line).unwrap_or(line.len());
    let code = &line[..comment_start];
    let first = take_while(code, 0, char::is_whitespace);

    if first > 0 {
        push_plain(line, 0, first, window_start, window_end, &mut spans);
    }

    if first < comment_start && code[first..].starts_with('[') {
        highlight_section(
            line,
            first,
            comment_start,
            window_start,
            window_end,
            &mut spans,
        );
    } else if let Some(equal) = equal_start(code) {
        highlight_key_value(
            line,
            first,
            equal,
            comment_start,
            window_start,
            window_end,
            &mut spans,
        );
    } else if first < comment_start {
        push_plain(
            line,
            first,
            comment_start,
            window_start,
            window_end,
            &mut spans,
        );
    }

    if comment_start < line.len() {
        push_span_window(
            &mut spans,
            line,
            comment_start,
            line.len(),
            string_style(),
            window_start,
            window_end,
        );
    }

    spans
}

fn highlight_section(
    line: &str,
    start: usize,
    end: usize,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) {
    let close = line[start..end]
        .rfind(']')
        .map(|relative| start + relative + 1)
        .unwrap_or(end);
    push_span_window(
        spans,
        line,
        start,
        start + 1,
        punctuation_style(),
        window_start,
        window_end,
    );
    if close > start + 1 {
        push_span_window(
            spans,
            line,
            start + 1,
            close.saturating_sub(1),
            key_style(),
            window_start,
            window_end,
        );
        push_span_window(
            spans,
            line,
            close.saturating_sub(1),
            close,
            punctuation_style(),
            window_start,
            window_end,
        );
    }
    if close < end {
        push_plain(line, close, end, window_start, window_end, spans);
    }
}

fn highlight_key_value(
    line: &str,
    first: usize,
    equal: usize,
    end: usize,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) {
    let key_end = trim_end_ascii_ws(line, first, equal);
    if first < key_end {
        push_span_window(
            spans,
            line,
            first,
            key_end,
            key_style(),
            window_start,
            window_end,
        );
    }
    if key_end < equal {
        push_plain(line, key_end, equal, window_start, window_end, spans);
    }
    push_span_window(
        spans,
        line,
        equal,
        equal + 1,
        punctuation_style(),
        window_start,
        window_end,
    );

    let value_start = take_while(line, equal + 1, char::is_whitespace).min(end);
    if equal + 1 < value_start {
        push_plain(
            line,
            equal + 1,
            value_start,
            window_start,
            window_end,
            spans,
        );
    }
    highlight_value(line, value_start, end, window_start, window_end, spans);
}

fn highlight_value(
    line: &str,
    start: usize,
    end: usize,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) {
    if start >= end {
        return;
    }

    let style = match line[start..end].chars().next() {
        Some('"' | '\'') => {
            highlight_string_segment_window(line, start, end, window_start, window_end, spans);
            return;
        }
        Some('[' | ']' | '{' | '}') => punctuation_style(),
        _ if is_toml_bool(&line[start..end]) => bool_style(),
        _ if is_toml_number_like(&line[start..end]) => number_style(),
        _ => plain_style(),
    };

    push_span_window(spans, line, start, end, style, window_start, window_end);
}

fn comment_start(line: &str) -> Option<usize> {
    let mut quote: Option<char> = None;
    let mut escaped = false;

    for (index, ch) in line.char_indices() {
        match quote {
            Some('"') => {
                if escaped {
                    escaped = false;
                } else if ch == '\\' {
                    escaped = true;
                } else if ch == '"' {
                    quote = None;
                }
            }
            Some('\'') => {
                if ch == '\'' {
                    quote = None;
                }
            }
            Some(_) => {}
            None => match ch {
                '"' | '\'' => quote = Some(ch),
                '#' => return Some(index),
                _ => {}
            },
        }
    }

    None
}

fn equal_start(line: &str) -> Option<usize> {
    let mut quote: Option<char> = None;
    let mut escaped = false;

    for (index, ch) in line.char_indices() {
        match quote {
            Some('"') => {
                if escaped {
                    escaped = false;
                } else if ch == '\\' {
                    escaped = true;
                } else if ch == '"' {
                    quote = None;
                }
            }
            Some('\'') => {
                if ch == '\'' {
                    quote = None;
                }
            }
            Some(_) => {}
            None => match ch {
                '"' | '\'' => quote = Some(ch),
                '=' => return Some(index),
                _ => {}
            },
        }
    }

    None
}

fn trim_end_ascii_ws(line: &str, start: usize, mut end: usize) -> usize {
    while end > start {
        let Some((previous, ch)) = line[..end].char_indices().next_back() else {
            break;
        };
        if !ch.is_ascii_whitespace() {
            break;
        }
        end = previous;
    }
    end
}

fn is_toml_bool(value: &str) -> bool {
    let value = value.trim_ascii_end();
    value == "true" || value == "false"
}

fn is_toml_number_like(value: &str) -> bool {
    let value = value.trim_ascii_end();
    value
        .chars()
        .next()
        .is_some_and(|ch| ch.is_ascii_digit() || ch == '-' || ch == '+')
}

fn push_plain(
    line: &str,
    start: usize,
    end: usize,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) {
    push_span_window(
        spans,
        line,
        start,
        end,
        plain_style(),
        window_start,
        window_end,
    );
}