use std::cmp::{max, min};
use std::io::{Result, Write};
use std::iter::zip;
use anstyle::Style;
use pulldown_cmark::{Alignment, CodeBlockKind, HeadingLevel};
use syntect::highlighting::HighlightState;
use syntect::parsing::{ParseState, ScopeStack};
use textwrap::core::{display_width, Word};
use textwrap::WordSeparator;
use crate::references::*;
use crate::render::data::{CurrentLine, CurrentTable, LinkReferenceDefinition, TableCell};
use crate::render::highlighting::highlighter;
use crate::render::state::*;
use crate::terminal::capabilities::{MarkCapability, StyleCapability, TerminalCapabilities};
use crate::terminal::osc::{clear_link, set_link_url};
use crate::terminal::TerminalSize;
use crate::theme::CombineStyle;
use crate::Theme;
use crate::{Environment, Settings};
pub fn write_indent<W: Write>(writer: &mut W, level: u16) -> Result<()> {
write!(writer, "{}", " ".repeat(level as usize))
}
pub fn write_styled<W: Write, S: AsRef<str>>(
writer: &mut W,
capabilities: &TerminalCapabilities,
style: &Style,
text: S,
) -> Result<()> {
match capabilities.style {
None => write!(writer, "{}", text.as_ref()),
Some(StyleCapability::Ansi) => write!(
writer,
"{}{}{}",
style.render(),
text.as_ref(),
style.render_reset()
),
}
}
fn write_remaining_lines<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
style: &Style,
indent: u16,
mut buffer: String,
next_lines: &[&[Word]],
last_line: &[Word],
) -> Result<CurrentLine> {
writeln!(writer)?;
write_indent(writer, indent)?;
for line in next_lines {
match line.split_last() {
None => {
}
Some((last, heads)) => {
for word in heads {
buffer.push_str(word.word);
buffer.push_str(word.whitespace);
}
buffer.push_str(last.word);
write_styled(writer, capabilities, style, &buffer)?;
writeln!(writer)?;
write_indent(writer, indent)?;
buffer.clear();
}
};
}
match last_line.split_last() {
None => {
Ok(CurrentLine::empty())
}
Some((last, heads)) => {
for word in heads {
buffer.push_str(word.word);
buffer.push_str(word.whitespace);
}
buffer.push_str(last.word);
write_styled(writer, capabilities, style, &buffer)?;
Ok(CurrentLine {
length: textwrap::core::display_width(&buffer) as u16,
trailing_space: Some(last.whitespace.to_owned()),
})
}
}
}
pub fn write_styled_and_wrapped<W: Write, S: AsRef<str>>(
writer: &mut W,
capabilities: &TerminalCapabilities,
style: &Style,
max_width: u16,
indent: u16,
current_line: CurrentLine,
text: S,
) -> Result<CurrentLine> {
let words = WordSeparator::UnicodeBreakProperties
.find_words(text.as_ref())
.collect::<Vec<_>>();
match words.first() {
None => Ok(current_line),
Some(first_word) => {
let current_width = current_line.length
+ indent
+ current_line
.trailing_space
.as_ref()
.map_or(0, |s| display_width(s.as_ref()) as u16);
if 0 < current_line.length
&& max_width < current_width + display_width(first_word) as u16
{
writeln!(writer)?;
write_indent(writer, indent)?;
return write_styled_and_wrapped(
writer,
capabilities,
style,
max_width,
indent,
CurrentLine::empty(),
text,
);
}
let widths = [
(max_width - current_width.min(max_width)) as f64,
(max_width - indent) as f64,
];
let lines = textwrap::wrap_algorithms::wrap_first_fit(&words, &widths);
match lines.split_first() {
None => {
Ok(current_line)
}
Some((first_line, tails)) => {
let mut buffer = String::with_capacity(max_width as usize);
let new_current_line = match first_line.split_last() {
None => {
current_line
}
Some((last, heads)) => {
if let Some(s) = current_line.trailing_space {
buffer.push_str(&s);
}
for word in heads {
buffer.push_str(word.word);
buffer.push_str(word.whitespace);
}
buffer.push_str(last.word);
let length =
current_line.length + textwrap::core::display_width(&buffer) as u16;
write_styled(writer, capabilities, style, &buffer)?;
buffer.clear();
CurrentLine {
length,
trailing_space: Some(last.whitespace.to_owned()),
}
}
};
match tails.split_last() {
None => {
Ok(new_current_line)
}
Some((last_line, next_lines)) => write_remaining_lines(
writer,
capabilities,
style,
indent,
buffer,
next_lines,
last_line,
),
}
}
}
}
}
}
pub fn write_mark<W: Write>(writer: &mut W, capabilities: &TerminalCapabilities) -> Result<()> {
if let Some(mark) = capabilities.marks {
match mark {
MarkCapability::ITerm2(marks) => marks.set_mark(writer),
}
} else {
Ok(())
}
}
pub fn write_rule<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
length: u16,
) -> std::io::Result<()> {
let rule = "\u{2550}".repeat(length as usize);
write_styled(
writer,
capabilities,
&Style::new().fg_color(Some(theme.rule_color)),
rule,
)
}
pub fn write_code_block_border<W: Write>(
writer: &mut W,
theme: &Theme,
capabilities: &TerminalCapabilities,
terminal_size: &TerminalSize,
) -> std::io::Result<()> {
let separator = "\u{2500}".repeat(terminal_size.columns.min(20) as usize);
write_styled(
writer,
capabilities,
&Style::new().fg_color(Some(theme.code_block_border_color)),
separator,
)?;
writeln!(writer)
}
pub fn write_link_refs<W: Write>(
writer: &mut W,
environment: &Environment,
capabilities: &TerminalCapabilities,
links: Vec<LinkReferenceDefinition>,
) -> Result<()> {
if !links.is_empty() {
writeln!(writer)?;
for link in links {
write_styled(
writer,
capabilities,
&link.style,
format!("[{}]: ", link.index),
)?;
if let Some(url) = environment.resolve_reference(&link.target) {
match &capabilities.style {
Some(StyleCapability::Ansi) => {
set_link_url(writer, url, &environment.hostname)?;
write_styled(writer, capabilities, &link.style, link.target)?;
clear_link(writer)?;
}
None => write_styled(writer, capabilities, &link.style, link.target)?,
};
} else {
write_styled(writer, capabilities, &link.style, link.target)?;
}
if !link.title.is_empty() {
write_styled(
writer,
capabilities,
&link.style,
format!(" {}", link.title),
)?;
}
writeln!(writer)?;
}
};
Ok(())
}
pub fn write_start_code_block<W: Write>(
writer: &mut W,
settings: &Settings,
indent: u16,
style: Style,
block_kind: CodeBlockKind<'_>,
) -> Result<StackedState> {
write_indent(writer, indent)?;
write_code_block_border(
writer,
&settings.theme,
&settings.terminal_capabilities,
&settings.terminal_size,
)?;
write_indent(writer, indent)?;
match (&settings.terminal_capabilities.style, block_kind) {
(Some(StyleCapability::Ansi), CodeBlockKind::Fenced(name)) if !name.is_empty() => {
match settings.syntax_set.find_syntax_by_token(&name) {
None => Ok(LiteralBlockAttrs {
indent,
style: settings.theme.code_style.on_top_of(&style),
}
.into()),
Some(syntax) => {
let parse_state = ParseState::new(syntax);
let highlight_state = HighlightState::new(highlighter(), ScopeStack::new());
Ok(HighlightBlockAttrs {
indent,
highlight_state,
parse_state,
}
.into())
}
}
}
(_, _) => Ok(LiteralBlockAttrs {
indent,
style: settings.theme.code_style.on_top_of(&style),
}
.into()),
}
}
pub fn write_start_heading<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
style: Style,
level: HeadingLevel,
) -> Result<StackedState> {
write_styled(
writer,
capabilities,
&style,
"\u{2504}".repeat(level as usize),
)?;
Ok(StackedState::Inline(
InlineState::InlineBlock,
InlineAttrs { style, indent: 0 },
))
}
fn calculate_column_widths(table: &CurrentTable) -> Option<Vec<usize>> {
let first_row = table.head.as_ref().or(table.rows.first())?;
let mut widths = vec![0; first_row.cells.len()];
let rows = table.head.iter().chain(table.rows.as_slice());
for row in rows {
let current = row.cells.as_slice().iter().map(|cell| {
cell.fragments
.as_slice()
.iter()
.fold(0, |acc, x| acc + x.len())
});
widths = zip(widths, current).map(|(a, b)| max(a, b)).collect();
}
Some(widths)
}
fn write_table_rule<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
length: u16,
) -> Result<()> {
let rule = "\u{2500}".repeat(length.into());
write_styled(writer, capabilities, &Style::new(), rule)?;
writeln!(writer)
}
fn format_table_cell(cell: TableCell, width: usize, alignment: Alignment) -> String {
use Alignment::*;
let content = cell.fragments.join("");
match alignment {
Left | None => format!(" {:<width$} ", content),
Center => format!(" {:^width$} ", content),
Right => format!(" {:>width$} ", content),
}
}
pub fn write_table<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
terminal_size: &TerminalSize,
table: CurrentTable,
) -> Result<()> {
if let Some(widths) = calculate_column_widths(&table) {
let total_width: usize = widths.iter().sum();
let rule_length = min(
(total_width + 2 * widths.len())
.try_into()
.unwrap_or(u16::MAX),
terminal_size.columns,
);
write_table_rule(writer, capabilities, rule_length)?;
if let Some(head) = table.head {
for ((cell, &width), &alignment) in zip(zip(head.cells, &widths), &table.alignments) {
write_styled(
writer,
capabilities,
&Style::new().bold(),
format_table_cell(cell, width, alignment),
)?;
}
writeln!(writer)?;
write_table_rule(writer, capabilities, rule_length)?;
}
for row in table.rows {
for ((cell, &width), &alignment) in zip(zip(row.cells, &widths), &table.alignments) {
write_styled(
writer,
capabilities,
&Style::new(),
format_table_cell(cell, width, alignment),
)?;
}
writeln!(writer)?;
}
write_table_rule(writer, capabilities, rule_length)?;
}
Ok(())
}