use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use cuj::facts::ProfileConfig;
pub struct Rendered {
pub raw: String,
pub lines: Vec<Line<'static>>,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Kind {
Tag,
Category,
Reference,
Url,
Bookmark,
Todo,
DoneTodo,
}
fn style(kind: Kind) -> Style {
match kind {
Kind::Tag => Style::new().fg(Color::Yellow),
Kind::Category => Style::new().fg(Color::Green),
Kind::Reference => Style::new().fg(Color::Cyan),
Kind::Url => Style::new()
.fg(Color::Blue)
.add_modifier(Modifier::UNDERLINED),
Kind::Bookmark => Style::new().fg(Color::Magenta),
Kind::Todo => Style::new().fg(Color::LightRed),
Kind::DoneTodo => Style::new().add_modifier(Modifier::DIM),
}
}
pub fn render(text: &str, profile: &ProfileConfig) -> Rendered {
let result = cuj::binding::parse(text, profile);
let mut paint: Vec<Option<Kind>> = vec![None; text.len()];
let mut mark = |loc: &Option<jots::Loc>, kind: Kind| {
if let Some(l) = loc {
for slot in paint.iter_mut().take(l.end.min(text.len())).skip(l.start) {
*slot = Some(kind);
}
}
};
for t in &result.todos {
mark(&t.loc, Kind::Todo);
}
for t in &result.done_todos {
mark(&t.loc, Kind::DoneTodo);
}
for t in result.todos.iter().chain(&result.done_todos) {
for r in &t.references {
mark(&r.loc, Kind::Reference);
}
for r in &t.cross_references {
mark(&r.loc, Kind::Reference);
}
for u in &t.urls {
mark(&u.loc, Kind::Url);
}
}
for r in &result.references {
mark(&r.loc, Kind::Reference);
}
for r in &result.cross_references {
mark(&r.loc, Kind::Reference);
}
for r in &result.file_references {
mark(&r.loc, Kind::Reference);
}
for r in &result.cross_file_references {
mark(&r.loc, Kind::Reference);
}
for r in &result.resources {
mark(&r.loc, Kind::Reference);
}
for r in &result.inter_jot_resources {
mark(&r.loc, Kind::Reference);
}
for u in &result.urls {
mark(&u.loc, Kind::Url);
}
for b in &result.bookmarks {
mark(&b.loc, Kind::Bookmark);
}
let mut names: Vec<(String, Kind)> = Vec::new();
let mut collect = |tags: &[String], cats: &[String]| {
for t in tags {
names.push((format!("..{t}"), Kind::Tag));
}
for c in cats {
names.push((format!("::{c}"), Kind::Category));
}
};
collect(&result.tags, &result.categories);
for t in result.todos.iter().chain(&result.done_todos) {
collect(&t.tags, &t.categories);
}
names.sort();
names.dedup();
for (needle, kind) in &names {
for start in occurrences(text, needle) {
for slot in paint.iter_mut().skip(start).take(needle.len()) {
*slot = Some(*kind);
}
}
}
let lines = to_lines(text, &paint);
Rendered {
raw: text.to_string(),
lines,
}
}
fn occurrences(text: &str, needle: &str) -> Vec<usize> {
let bytes = text.as_bytes();
let token = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b':' || b == b'.';
let mut out = Vec::new();
let mut from = 0;
while let Some(i) = text[from..].find(needle) {
let start = from + i;
let end = start + needle.len();
let left_ok = start == 0 || !token(bytes[start - 1]);
let right_ok = end >= bytes.len() || !token(bytes[end]);
if left_ok && right_ok {
out.push(start);
}
from = start + 1;
}
out
}
fn to_lines(text: &str, paint: &[Option<Kind>]) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let mut offset = 0;
for raw_line in text.split('\n') {
let mut spans: Vec<Span<'static>> = Vec::new();
let mut run_start = 0;
let mut run_kind: Option<Kind> = None;
let line_paint = &paint[offset..offset + raw_line.len()];
for (i, k) in line_paint.iter().enumerate() {
if *k != run_kind {
if i > run_start {
spans.push(span(&raw_line[run_start..i], run_kind));
}
run_start = i;
run_kind = *k;
}
}
if raw_line.len() > run_start {
spans.push(span(&raw_line[run_start..], run_kind));
}
lines.push(Line::from(spans));
offset += raw_line.len() + 1;
}
if text.ends_with('\n') {
lines.pop();
}
lines
}
fn span(s: &str, kind: Option<Kind>) -> Span<'static> {
match kind {
Some(k) => Span::styled(s.to_string(), style(k)),
None => Span::raw(s.to_string()),
}
}