#![warn(missing_docs)]
mod border;
mod code;
mod drollery;
mod illuminate;
mod scribe;
mod style;
use std::collections::HashSet;
use figlet_rs::FIGfont;
pub use border::Border;
pub use style::Theme;
use crate::illuminate::{display_width, illuminate_paragraph, Line, Options};
use crate::style::Style;
pub const MIN_WIDTH: usize = 24;
const MARGIN_RULE: char = '┊';
const MARGIN_SEP_W: usize = 3;
const DROLLERY_GAP: usize = 3;
const COLUMN_GUTTER: usize = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DropCap {
First,
All,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Font {
Blackletter,
Standard,
}
#[derive(Clone, Debug)]
pub struct Config {
pub width: usize,
pub border: Border,
pub theme: Theme,
pub colored: bool,
pub drop_cap: DropCap,
pub font: Font,
pub rubrics: HashSet<String>,
pub drolleries: bool,
pub seed: u64,
pub pilcrows: bool,
pub justify: bool,
pub hyphenate: bool,
pub fillers: bool,
pub incipit: bool,
pub columns: usize,
pub code: bool,
pub language: Option<String>,
pub corrupt: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
width: 60,
border: Border::Ornate,
theme: Theme::Gold,
colored: false,
drop_cap: DropCap::First,
font: Font::Blackletter,
rubrics: HashSet::new(),
drolleries: false,
seed: 0,
pilcrows: false,
justify: false,
hyphenate: false,
fillers: false,
incipit: false,
columns: 1,
code: false,
language: None,
corrupt: false,
}
}
}
#[must_use]
pub fn detect_language(filename: &str) -> Option<String> {
let ext = std::path::Path::new(filename).extension()?.to_str()?;
code::by_extension(ext).map(|lang| lang.name.to_string())
}
#[must_use]
pub fn render(source: &str, cfg: &Config) -> String {
let width = cfg.width.max(MIN_WIDTH);
let style = Style::new(cfg.colored, cfg.theme);
let corrupted;
let source = if cfg.corrupt {
corrupted = scribe::corrupt(source, cfg.seed);
corrupted.as_str()
} else {
source
};
let (body, body_w) = if cfg.code {
let lang = cfg
.language
.as_deref()
.and_then(code::by_name)
.unwrap_or_else(code::generic);
code::illuminate(source, lang, &style, width)
} else {
lay_prose(source, cfg, width, &style)
};
frame(&body, body_w, cfg, &style)
}
#[must_use]
pub fn render_glossed(rows: &[(String, String)], cfg: &Config) -> String {
let width = cfg.width.max(MIN_WIDTH);
let style = Style::new(cfg.colored, cfg.theme);
let lang = cfg
.language
.as_deref()
.and_then(code::by_name)
.unwrap_or_else(code::generic);
let (body, body_w) = code::lay_rows(rows, lang, &style, width);
frame(&body, body_w, cfg, &style)
}
fn lay_prose(source: &str, cfg: &Config, width: usize, style: &Style) -> (Vec<Line>, usize) {
let columns = cfg.columns.max(1);
let font = load_font(cfg.font);
let col_w = if columns >= 2 {
(width.saturating_sub((columns - 1) * COLUMN_GUTTER) / columns).max(1)
} else {
width
};
let opts = Options {
width: col_w,
gap: 1,
style,
rubrics: &cfg.rubrics,
justify: cfg.justify,
hyphenate: cfg.hyphenate,
fillers: cfg.fillers,
};
let content = lay_body(source, cfg, &font, &opts);
if columns >= 2 {
let laid = illuminate::lay_in_columns(&content, columns, col_w, COLUMN_GUTTER);
let total = columns * col_w + (columns - 1) * COLUMN_GUTTER;
(laid, total)
} else {
(content, width)
}
}
fn frame(body: &[Line], body_w: usize, cfg: &Config, style: &Style) -> String {
let rows = if cfg.drolleries && !body.is_empty() {
let (margin, margin_w) = scatter_drolleries(body.len(), cfg.seed, style);
let sep = format!(" {} ", style.border(&MARGIN_RULE.to_string()));
let merged = illuminate::merge_columns(&margin, body, margin_w, body_w, &sep, MARGIN_SEP_W);
let total = margin_w + MARGIN_SEP_W + body_w;
border::render(&merged, total, cfg.border, style)
} else {
border::render(body, body_w, cfg.border, style)
};
rows.join("\n")
}
fn load_font(font: Font) -> FIGfont {
match font {
Font::Standard => FIGfont::standard().expect("built-in FIGlet standard font"),
Font::Blackletter => FIGfont::from_content(include_str!("../fonts/fraktur.flf"))
.expect("embedded Fraktur font is valid"),
}
}
fn lay_body(source: &str, cfg: &Config, font: &FIGfont, opts: &Options) -> Vec<Line> {
let source = source.replace("\r\n", "\n").replace('\r', "\n");
let split: Vec<&str> = source
.split("\n\n")
.map(str::trim)
.filter(|p| !p.is_empty())
.collect();
let joined;
let paragraphs: Vec<&str> = if cfg.pilcrows {
joined = split.join(" ¶ ");
vec![joined.as_str()]
} else {
split
};
let mut content: Vec<Line> = Vec::new();
for (i, para) in paragraphs.iter().enumerate() {
let drop_cap = match cfg.drop_cap {
DropCap::All => true,
DropCap::None => false,
DropCap::First => i == 0,
};
if i > 0 {
content.push(Line {
shown: String::new(),
len: 0,
}); }
let incipit = cfg.incipit && i == 0;
content.extend(illuminate_paragraph(para, drop_cap, incipit, font, opts));
}
content
}
fn scatter_drolleries(height: usize, seed: u64, style: &Style) -> (Vec<Line>, usize) {
let margin_w = drollery::max_width();
let mut margin: Vec<Line> = (0..height)
.map(|_| Line {
shown: String::new(),
len: 0,
})
.collect();
let mut idx = 0;
let mut nth = 0u64;
while idx < margin.len() {
let figure = drollery::pick(seed, nth);
for (r, row) in figure.iter().enumerate() {
if idx + r >= margin.len() {
break;
}
margin[idx + r] = Line {
shown: style.border(row),
len: display_width(row),
};
}
idx += figure.len() + DROLLERY_GAP;
nth += 1;
}
(margin, margin_w)
}