use failure::Error;
use pulldown_cmark::Event::*;
use pulldown_cmark::Tag::*;
use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag};
use std::error::Error as StdError;
use std::fs::File;
use std::io::prelude::*;
use std::io::stdin;
use std::path::{Path, PathBuf};
use syntect::parsing::SyntaxSet;
use crate::resources::ResourceAccess;
use crate::terminal::TerminalCapabilities;
pub use crate::terminal::*;
use ansi_term::{Colour, Style};
use std::collections::VecDeque;
use std::io;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Theme, ThemeSet};
pub fn process_file(filename: &str) -> Result<(), Box<dyn StdError>> {
let (base_dir, input) = read_input(filename)?;
let mut options = Options::empty();
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_STRIKETHROUGH);
let parser = Parser::new_ext(&input, options);
let syntax_set = SyntaxSet::load_defaults_newlines();
let size = TerminalSize::detect().unwrap_or_default();
push_tty(
&mut std::io::stdout(),
&TerminalCapabilities::detect(),
TerminalSize {
width: size.width,
..size
},
parser,
&base_dir,
ResourceAccess::RemoteAllowed,
syntax_set,
)?;
Ok(())
}
fn read_input<T: AsRef<str>>(filename: T) -> std::io::Result<(PathBuf, String)> {
let cd = std::env::current_dir()?;
let mut buffer = String::new();
if filename.as_ref() == "-" {
stdin().read_to_string(&mut buffer)?;
Ok((cd, buffer))
} else {
let mut source = File::open(filename.as_ref())?;
source.read_to_string(&mut buffer)?;
let base_dir = cd
.join(filename.as_ref())
.parent()
.map(|p| p.to_path_buf())
.unwrap_or(cd);
Ok((base_dir, buffer))
}
}
pub fn dump_events<'a, W, I>(writer: &mut W, events: I) -> Result<(), Error>
where
I: Iterator<Item = Event<'a>>,
W: Write,
{
for event in events {
writeln!(writer, "{:?}", event)?;
}
Ok(())
}
pub fn push_tty<'a, 'e, W, I>(
writer: &'a mut W,
capabilities: &TerminalCapabilities,
size: TerminalSize,
mut events: I,
base_dir: &'a Path,
resource_access: ResourceAccess,
syntax_set: SyntaxSet,
) -> Result<(), Error>
where
I: Iterator<Item = Event<'e>>,
W: Write,
{
let theme = &ThemeSet::load_defaults().themes["Solarized (dark)"];
events
.try_fold(
Context::new(
writer,
capabilities,
size,
base_dir,
resource_access,
syntax_set,
theme,
),
write_event,
)?
.write_pending_links()?;
Ok(())
}
#[derive(Debug, PartialEq)]
enum BlockLevel {
Block,
Inline,
}
#[derive(Debug)]
enum ListItemKind {
Unordered,
Ordered(u64),
}
#[derive(Debug)]
struct Link<'a> {
index: usize,
destination: CowStr<'a>,
title: CowStr<'a>,
}
struct ResourceContext<'a> {
base_dir: &'a Path,
resource_access: ResourceAccess,
}
impl ResourceContext<'_> {
fn resolve_reference(&self, reference: &str) -> Option<url::Url> {
use url::Url;
Url::parse(reference)
.or_else(|_| Url::from_file_path(self.base_dir.join(reference)))
.ok()
}
}
struct OutputContext<'a, W: Write> {
size: TerminalSize,
writer: &'a mut W,
capabilities: &'a TerminalCapabilities,
}
#[derive(Debug)]
struct StyleContext {
current: Style,
previous: Vec<Style>,
emphasis_level: usize,
}
#[derive(Debug)]
struct BlockContext {
indent_level: usize,
level: BlockLevel,
}
#[derive(Debug)]
struct LinkContext<'a> {
pending_links: VecDeque<Link<'a>>,
next_link_index: usize,
current_link_type: Option<LinkType>,
inside_inline_link: bool,
}
struct CodeContext<'a> {
syntax_set: SyntaxSet,
theme: &'a Theme,
current_highlighter: Option<HighlightLines<'a>>,
}
#[derive(Debug)]
struct ImageContext {
inline_image: bool,
}
struct Context<'io, 'c, 'l, W: Write> {
resources: ResourceContext<'io>,
output: OutputContext<'io, W>,
style: StyleContext,
block: BlockContext,
links: LinkContext<'l>,
code: CodeContext<'c>,
image: ImageContext,
list_item_kind: Vec<ListItemKind>,
}
impl<'io, 'c, 'l, W: Write> Context<'io, 'c, 'l, W> {
fn new(
writer: &'io mut W,
capabilities: &'io TerminalCapabilities,
size: TerminalSize,
base_dir: &'io Path,
resource_access: ResourceAccess,
syntax_set: SyntaxSet,
theme: &'c Theme,
) -> Context<'io, 'c, 'l, W> {
Context {
resources: ResourceContext {
base_dir,
resource_access,
},
output: OutputContext {
size,
writer,
capabilities,
},
style: StyleContext {
current: Style::new(),
previous: Vec::new(),
emphasis_level: 0,
},
block: BlockContext {
indent_level: 0,
level: BlockLevel::Inline,
},
links: LinkContext {
pending_links: VecDeque::new(),
next_link_index: 1,
current_link_type: None,
inside_inline_link: false,
},
code: CodeContext {
syntax_set,
theme,
current_highlighter: None,
},
image: ImageContext {
inline_image: false,
},
list_item_kind: Vec::new(),
}
}
fn start_inline_text(&mut self) -> io::Result<()> {
if let BlockLevel::Block = self.block.level {
self.newline_and_indent()?
};
self.block.level = BlockLevel::Inline;
Ok(())
}
fn end_inline_text_with_margin(&mut self) -> io::Result<()> {
if let BlockLevel::Inline = self.block.level {
self.newline()?;
};
self.block.level = BlockLevel::Block;
Ok(())
}
fn newline(&mut self) -> io::Result<()> {
writeln!(self.output.writer)
}
fn newline_and_indent(&mut self) -> io::Result<()> {
self.newline()?;
self.indent()
}
fn indent(&mut self) -> io::Result<()> {
write!(
self.output.writer,
"{}",
" ".repeat(self.block.indent_level)
)
.map_err(Into::into)
}
fn set_style(&mut self, style: Style) {
self.style.previous.push(self.style.current);
self.style.current = style;
}
fn drop_style(&mut self) {
match self.style.previous.pop() {
Some(old) => self.style.current = old,
None => self.style.current = Style::new(),
};
}
fn write_styled<S: AsRef<str>>(&mut self, style: &Style, text: S) -> io::Result<()> {
match self.output.capabilities.style {
StyleCapability::None => write!(self.output.writer, "{}", text.as_ref())?,
StyleCapability::Ansi(ref ansi) => {
ansi.write_styled(self.output.writer, style, text)?
}
}
Ok(())
}
fn write_styled_current<S: AsRef<str>>(&mut self, text: S) -> io::Result<()> {
let style = self.style.current;
self.write_styled(&style, text)
}
fn enable_emphasis(&mut self) {
self.style.emphasis_level += 1;
let is_italic = self.style.emphasis_level % 2 == 1;
{
let new_style = Style {
is_italic,
..self.style.current
};
self.set_style(new_style);
}
}
fn add_link(&mut self, destination: CowStr<'l>, title: CowStr<'l>) -> usize {
let index = self.links.next_link_index;
self.links.next_link_index += 1;
self.links.pending_links.push_back(Link {
index,
destination,
title,
});
index
}
fn write_pending_links(&mut self) -> Result<(), Error> {
if !self.links.pending_links.is_empty() {
self.newline()?;
let link_style = self.style.current.fg(Colour::Blue);
while let Some(link) = self.links.pending_links.pop_front() {
let link_text = format!("[{}]: {} {}", link.index, link.destination, link.title);
self.write_styled(&link_style, link_text)?;
self.newline()?
}
};
Ok(())
}
fn write_border(&mut self) -> io::Result<()> {
let separator = "\u{2500}".repeat(self.output.size.width.min(20));
let style = self.style.current.fg(Colour::Green);
self.write_styled(&style, separator)?;
self.newline()
}
fn write_highlighted(&mut self, text: CowStr<'l>) -> io::Result<()> {
let mut wrote_highlighted: bool = false;
if let Some(ref mut highlighter) = self.code.current_highlighter {
if let StyleCapability::Ansi(ref ansi) = self.output.capabilities.style {
let regions = highlighter.highlight(&text, &self.code.syntax_set);
highlighting::write_as_ansi(self.output.writer, ansi, ®ions)?;
wrote_highlighted = true;
}
}
if !wrote_highlighted {
self.write_styled_current(&text)?;
}
Ok(())
}
fn set_mark_if_supported(&mut self) -> io::Result<()> {
match self.output.capabilities.marks {
MarkCapability::ITerm2(ref marks) => marks.set_mark(self.output.writer),
MarkCapability::None => Ok(()),
}
}
}
fn write_event<'io, 'c, 'l, W: Write>(
mut ctx: Context<'io, 'c, 'l, W>,
event: Event<'l>,
) -> Result<Context<'io, 'c, 'l, W>, Error> {
match event {
SoftBreak | HardBreak => {
ctx.newline_and_indent()?;
Ok(ctx)
}
Rule => {
ctx.start_inline_text()?;
let rule = "\u{2550}".repeat(ctx.output.size.width as usize);
let style = ctx.style.current.fg(Colour::Green);
ctx.write_styled(&style, rule)?;
ctx.end_inline_text_with_margin()?;
Ok(ctx)
}
Code(code) => {
ctx.write_styled(&ctx.style.current.fg(Colour::Yellow), code)?;
Ok(ctx)
}
Text(text) => {
if !ctx.image.inline_image {
ctx.write_highlighted(text)?;
}
Ok(ctx)
}
TaskListMarker(checked) => {
let marker = if checked { "\u{2611} " } else { "\u{2610} " };
ctx.write_highlighted(CowStr::Borrowed(marker))?;
Ok(ctx)
}
Start(tag) => start_tag(ctx, tag),
End(tag) => end_tag(ctx, tag),
Html(content) => {
let html_style = ctx.style.current.fg(Colour::Green);
ctx.write_styled(&html_style, content)?;
Ok(ctx)
}
FootnoteReference(_) => panic!("mdcat does not support footnotes"),
}
}
fn start_tag<'io, 'c, 'l, W: Write>(
mut ctx: Context<'io, 'c, 'l, W>,
tag: Tag<'l>,
) -> Result<Context<'io, 'c, 'l, W>, Error> {
match tag {
Paragraph => ctx.start_inline_text()?,
Heading(level) => {
ctx.write_pending_links()?;
ctx.start_inline_text()?;
ctx.set_mark_if_supported()?;
ctx.set_style(Style::new().fg(Colour::Blue).bold());
ctx.write_styled_current("\u{2504}".repeat(level as usize))?
}
BlockQuote => {
ctx.block.indent_level += 4;
ctx.start_inline_text()?;
ctx.enable_emphasis();
ctx.style.current = ctx.style.current.fg(Colour::Green);
}
CodeBlock(name) => {
ctx.start_inline_text()?;
ctx.write_border()?;
ctx.code.current_highlighter = if name.is_empty() {
None
} else {
ctx.code
.syntax_set
.find_syntax_by_token(&name)
.map(|syntax| HighlightLines::new(syntax, ctx.code.theme))
};
if ctx.code.current_highlighter.is_none() {
let style = ctx.style.current.fg(Colour::Yellow);
ctx.set_style(style);
}
}
List(kind) => {
ctx.list_item_kind.push(match kind {
Some(start) => ListItemKind::Ordered(start),
None => ListItemKind::Unordered,
});
ctx.newline()?;
}
Item => {
ctx.indent()?;
ctx.block.level = BlockLevel::Inline;
match ctx.list_item_kind.pop() {
Some(ListItemKind::Unordered) => {
write!(ctx.output.writer, "\u{2022} ")?;
ctx.block.indent_level += 2;
ctx.list_item_kind.push(ListItemKind::Unordered);
}
Some(ListItemKind::Ordered(number)) => {
write!(ctx.output.writer, "{:>2}. ", number)?;
ctx.block.indent_level += 4;
ctx.list_item_kind.push(ListItemKind::Ordered(number + 1));
}
None => panic!("List item without list item kind"),
}
}
FootnoteDefinition(_) => panic!("mdcat does not support footnotes"),
Table(_) | TableHead | TableRow | TableCell => panic!("mdcat does not support tables"),
Strikethrough => {
let style = ctx.style.current.strikethrough();
ctx.set_style(style)
}
Emphasis => ctx.enable_emphasis(),
Strong => {
let style = ctx.style.current.bold();
ctx.set_style(style)
}
Link(link_type, destination, _) => {
ctx.links.current_link_type = Some(link_type);
match ctx.output.capabilities.links {
LinkCapability::OSC8(ref osc8) => {
if let Some(url) = ctx.resources.resolve_reference(&destination) {
osc8.set_link_url(ctx.output.writer, url)?;
ctx.links.inside_inline_link = true;
}
}
LinkCapability::None => {
let _ = destination;
}
}
}
Image(_, link, _title) => match ctx.output.capabilities.image {
ImageCapability::Terminology(ref terminology) => {
let access = ctx.resources.resource_access;
if let Some(url) = ctx
.resources
.resolve_reference(&link)
.filter(|url| access.permits(url))
{
terminology.write_inline_image(
&mut ctx.output.writer,
ctx.output.size,
&url,
)?;
ctx.image.inline_image = true;
}
}
ImageCapability::ITerm2(ref iterm2) => {
let access = ctx.resources.resource_access;
if let Some(url) = ctx
.resources
.resolve_reference(&link)
.filter(|url| access.permits(url))
{
if let Ok(contents) = iterm2.read_and_render(&url) {
iterm2.write_inline_image(ctx.output.writer, url.as_str(), &contents)?;
ctx.image.inline_image = true;
}
}
}
ImageCapability::Kitty(ref kitty) => {
let access = ctx.resources.resource_access;
if let Some(url) = ctx
.resources
.resolve_reference(&link)
.filter(|url| access.permits(url))
{
if let Ok(kitty_image) = kitty.read_and_render(&url) {
kitty.write_inline_image(ctx.output.writer, kitty_image)?;
ctx.image.inline_image = true;
}
}
}
ImageCapability::None => {
let _ = link;
}
},
};
Ok(ctx)
}
fn end_tag<'io, 'c, 'l, W: Write>(
mut ctx: Context<'io, 'c, 'l, W>,
tag: Tag<'l>,
) -> Result<Context<'io, 'c, 'l, W>, Error> {
match tag {
Paragraph => ctx.end_inline_text_with_margin()?,
Heading(_) => {
ctx.drop_style();
ctx.end_inline_text_with_margin()?
}
BlockQuote => {
ctx.block.indent_level -= 4;
ctx.style.emphasis_level -= 1;
ctx.drop_style();
ctx.end_inline_text_with_margin()?
}
CodeBlock(_) => {
match ctx.code.current_highlighter {
None => ctx.drop_style(),
Some(_) => {
ctx.code.current_highlighter = None;
}
}
ctx.write_border()?;
ctx.block.level = BlockLevel::Block;
}
List(_) => {
ctx.list_item_kind.pop();
ctx.end_inline_text_with_margin()?;
}
Item => {
match ctx.list_item_kind.last() {
Some(&ListItemKind::Ordered(_)) => ctx.block.indent_level -= 4,
Some(&ListItemKind::Unordered) => ctx.block.indent_level -= 2,
None => (),
}
ctx.end_inline_text_with_margin()?
}
FootnoteDefinition(_) | Table(_) | TableHead | TableRow | TableCell => {}
Strikethrough => ctx.drop_style(),
Emphasis => {
ctx.drop_style();
ctx.style.emphasis_level -= 1;
}
Strong => ctx.drop_style(),
Link(_, destination, title) => {
if ctx.links.inside_inline_link {
match ctx.output.capabilities.links {
LinkCapability::OSC8(ref osc8) => {
osc8.clear_link(ctx.output.writer)?;
}
LinkCapability::None => {}
}
ctx.links.inside_inline_link = false;
} else {
match ctx.links.current_link_type {
Some(LinkType::Autolink) | Some(LinkType::Email) => {
}
_ => {
let index = ctx.add_link(destination, title);
let style = ctx.style.current.fg(Colour::Blue);
ctx.write_styled(&style, format!("[{}]", index))?
}
}
}
}
Image(_, link, _) => {
if !ctx.image.inline_image {
let style = ctx.style.current.fg(Colour::Blue);
ctx.write_styled(&style, format!(" ({})", link))?
}
ctx.image.inline_image = false;
}
};
Ok(ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use pulldown_cmark::Parser;
fn render_string(
input: &str,
base_dir: &Path,
resource_access: ResourceAccess,
syntax_set: SyntaxSet,
capabilities: TerminalCapabilities,
size: TerminalSize,
) -> Result<Vec<u8>, Error> {
let source = Parser::new(input);
let mut sink = Vec::new();
push_tty(
&mut sink,
&capabilities,
size,
source,
base_dir,
resource_access,
syntax_set,
)?;
Ok(sink)
}
#[test]
#[allow(non_snake_case)]
fn GH_49_format_no_colour_simple() {
let result = String::from_utf8(
render_string(
"_lorem_ **ipsum** dolor **sit** _amet_",
Path::new("/"),
ResourceAccess::LocalOnly,
SyntaxSet::default(),
TerminalCapabilities::none(),
TerminalSize::default(),
)
.unwrap(),
)
.unwrap();
assert_eq!(result, "lorem ipsum dolor sit amet\n");
}
}