pulldown_cmark_mdcat/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
// Copyright 2018-2020 Sebastian Wiesner <sebastian@swsnr.de>
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//! Write markdown to TTYs.
//!
//! See [`push_tty`] for the main entry point.
//!
//! ## MSRV
//!
//! This library generally supports only the latest stable Rust version.
//!
//! ## Features
//!
//! - `default` enables `svg` and `image-processing`.
//!
//! - `svg` includes support for rendering SVG images to PNG for terminals which do not support SVG
//! images natively. This feature adds a dependency on `resvg`.
//!
//! - `image-processing` enables processing of pixel images before rendering. This feature adds
//! a dependency on `image`. If disabled mdcat will not be able to render inline images on some
//! terminals, or render images incorrectly or at wrong sizes on other terminals.
//!
//! Do not disable this feature unless you are sure that you won't use inline images, or accept
//! incomplete rendering of images. Please do not report issues with inline images with this
//! feature disabled.
//!
//! This feature only exists to allow building with minimal dependencies for use cases where
//! inline image support is not used or required. Do not disable this feature unless you know
//! you won't use inline images, or can accept buggy inline image rendering.
//!
//! Please **do not report bugs** about inline image rendering with this feature disabled, unless
//! the issue can also be reproduced if the feature is enabled.
#![deny(warnings, missing_docs, clippy::all)]
#![forbid(unsafe_code)]
use std::io::{Error, ErrorKind, Result, Write};
use std::path::Path;
use gethostname::gethostname;
use pulldown_cmark::Event;
use syntect::parsing::SyntaxSet;
use tracing::instrument;
use url::Url;
pub use crate::resources::ResourceUrlHandler;
pub use crate::terminal::capabilities::TerminalCapabilities;
pub use crate::terminal::{TerminalProgram, TerminalSize};
pub use crate::theme::Theme;
mod references;
pub mod resources;
pub mod terminal;
mod theme;
mod render;
/// Settings for markdown rendering.
#[derive(Debug)]
pub struct Settings<'a> {
/// Capabilities of the terminal mdcat writes to.
pub terminal_capabilities: TerminalCapabilities,
/// The size of the terminal mdcat writes to.
pub terminal_size: TerminalSize,
/// Syntax set for syntax highlighting of code blocks.
pub syntax_set: &'a SyntaxSet,
/// Colour theme for mdcat
pub theme: Theme,
}
/// The environment to render markdown in.
#[derive(Debug)]
pub struct Environment {
/// The base URL to resolve relative URLs with.
pub base_url: Url,
/// The local host name.
pub hostname: String,
}
impl Environment {
/// Create an environment for the local host with the given `base_url`.
///
/// Take the local hostname from `gethostname`.
pub fn for_localhost(base_url: Url) -> Result<Self> {
gethostname()
.into_string()
.map_err(|raw| {
Error::new(
ErrorKind::InvalidData,
format!("gethostname() returned invalid unicode data: {raw:?}"),
)
})
.map(|hostname| Environment { base_url, hostname })
}
/// Create an environment for a local directory.
///
/// Convert the directory to a directory URL, and obtain the hostname from `gethostname`.
///
/// `base_dir` must be an absolute path; return an IO error with `ErrorKind::InvalidInput`
/// otherwise.
pub fn for_local_directory<P: AsRef<Path>>(base_dir: &P) -> Result<Self> {
Url::from_directory_path(base_dir)
.map_err(|_| {
Error::new(
ErrorKind::InvalidInput,
format!(
"Base directory {} must be an absolute path",
base_dir.as_ref().display()
),
)
})
.and_then(Self::for_localhost)
}
}
/// Write markdown to a TTY.
///
/// Iterate over Markdown AST `events`, format each event for TTY output and
/// write the result to a `writer`, using the given `settings` and `environment`
/// for rendering and resource access.
///
/// `push_tty` tries to limit output to the given number of TTY `columns` but
/// does not guarantee that output stays within the column limit.
#[instrument(level = "debug", skip_all, fields(environment.hostname = environment.hostname.as_str(), environment.base_url = &environment.base_url.as_str()))]
pub fn push_tty<'a, 'e, W, I>(
settings: &Settings,
environment: &Environment,
resource_handler: &dyn ResourceUrlHandler,
writer: &'a mut W,
mut events: I,
) -> Result<()>
where
I: Iterator<Item = Event<'e>>,
W: Write,
{
use render::*;
let StateAndData(final_state, final_data) = events.try_fold(
StateAndData(State::default(), StateData::default()),
|StateAndData(state, data), event| {
write_event(
writer,
settings,
environment,
&resource_handler,
state,
data,
event,
)
},
)?;
finish(writer, settings, environment, final_state, final_data)
}
#[cfg(test)]
mod tests {
use pulldown_cmark::Parser;
use crate::resources::NoopResourceHandler;
use super::*;
fn render_string(input: &str, settings: &Settings) -> Result<String> {
let source = Parser::new(input);
let mut sink = Vec::new();
let env =
Environment::for_local_directory(&std::env::current_dir().expect("Working directory"))?;
push_tty(settings, &env, &NoopResourceHandler, &mut sink, source)?;
Ok(String::from_utf8_lossy(&sink).into())
}
fn render_string_dumb(markup: &str) -> Result<String> {
render_string(
markup,
&Settings {
syntax_set: &SyntaxSet::default(),
terminal_capabilities: TerminalProgram::Dumb.capabilities(),
terminal_size: TerminalSize::default(),
theme: Theme::default(),
},
)
}
mod layout {
use super::render_string_dumb;
use insta::assert_snapshot;
#[test]
#[allow(non_snake_case)]
fn GH_49_format_no_colour_simple() {
assert_eq!(
render_string_dumb("_lorem_ **ipsum** dolor **sit** _amet_").unwrap(),
"lorem ipsum dolor sit amet\n",
)
}
#[test]
fn begins_with_rule() {
assert_snapshot!(render_string_dumb("----").unwrap())
}
#[test]
fn begins_with_block_quote() {
assert_snapshot!(render_string_dumb("> Hello World").unwrap());
}
#[test]
fn rule_in_block_quote() {
assert_snapshot!(render_string_dumb(
"> Hello World
> ----"
)
.unwrap());
}
#[test]
fn heading_in_block_quote() {
assert_snapshot!(render_string_dumb(
"> Hello World
> # Hello World"
)
.unwrap())
}
#[test]
fn heading_levels() {
assert_snapshot!(render_string_dumb(
"
# First
## Second
### Third"
)
.unwrap())
}
#[test]
fn autolink_creates_no_reference() {
assert_eq!(
render_string_dumb("Hello <http://example.com>").unwrap(),
"Hello http://example.com\n"
)
}
#[test]
fn flush_ref_links_before_toplevel_heading() {
assert_snapshot!(render_string_dumb(
"> Hello [World](http://example.com/world)
> # No refs before this headline
# But before this"
)
.unwrap())
}
#[test]
fn flush_ref_links_at_end() {
assert_snapshot!(render_string_dumb(
"Hello [World](http://example.com/world)
# Headline
Hello [Donald](http://example.com/Donald)"
)
.unwrap())
}
}
mod disabled_features {
use insta::assert_snapshot;
use super::render_string_dumb;
#[test]
#[allow(non_snake_case)]
fn GH_155_do_not_choke_on_footnotes() {
assert_snapshot!(render_string_dumb(
"A footnote [^1]
[^1: We do not support footnotes."
)
.unwrap())
}
}
}