#![cfg_attr(feature = "clippy", feature(plugin))]
#![cfg_attr(feature = "clippy", plugin(clippy))]
#![deny(missing_docs)]
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadMe;
#[macro_use]
extern crate html5ever;
extern crate unicode_width;
#[macro_use]
mod macros;
pub mod render;
#[cfg(feature = "css")]
pub mod css;
use render::text_renderer::{
PlainDecorator, RenderLine, RichAnnotation, RichDecorator, SubRenderer, TaggedLine,
TextDecorator, TextRenderer,
};
use render::Renderer;
use html5ever::driver::ParseOpts;
use html5ever::parse_document;
use html5ever::tendril::TendrilSink;
use html5ever::tree_builder::TreeBuilderOpts;
mod markup5ever_rcdom;
use markup5ever_rcdom::{
Handle,
NodeData::{Comment, Document, Element},
RcDom,
};
use std::cell::Cell;
use std::cmp::{max, min};
#[cfg(feature = "css")]
use lightningcss::values::color::CssColor;
#[cfg(feature = "css")]
use std::convert::TryFrom;
use std::io;
use std::io::Write;
use std::iter::{once, repeat};
#[allow(unused)] use std::ops::Deref;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Colour {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[cfg(feature = "css")]
impl TryFrom<&CssColor> for Colour {
type Error = ();
fn try_from(value: &CssColor) -> std::result::Result<Self, Self::Error> {
match value {
CssColor::RGBA(rgba) => {
Ok(Colour {
r: rgba.red,
g: rgba.green,
b: rgba.blue,
})
}
_ => Err(()),
}
}
}
#[derive(thiserror::Error, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Error {
#[error("Output width not wide enough.")]
TooNarrow,
#[cfg(feature = "css")]
#[error("Invalid CSS")]
CssParseError,
#[error("Unknown failure")]
Fail,
#[error("Formatting error")]
FmtError(#[from] std::fmt::Error),
}
type Result<T> = std::result::Result<T, Error>;
struct Discard {}
impl Write for Discard {
fn write(&mut self, bytes: &[u8]) -> std::result::Result<usize, io::Error> {
Ok(bytes.len())
}
fn flush(&mut self) -> std::result::Result<(), io::Error> {
Ok(())
}
}
const MIN_WIDTH: usize = 3;
#[derive(Debug, Copy, Clone)]
pub struct SizeEstimate {
size: usize, min_width: usize, }
impl Default for SizeEstimate {
fn default() -> SizeEstimate {
SizeEstimate {
size: 0,
min_width: 0,
}
}
}
impl SizeEstimate {
pub fn add(self, other: SizeEstimate) -> SizeEstimate {
SizeEstimate {
size: self.size + other.size,
min_width: max(self.min_width, other.min_width),
}
}
pub fn max(self, other: SizeEstimate) -> SizeEstimate {
SizeEstimate {
size: max(self.size, other.size),
min_width: max(self.min_width, other.min_width),
}
}
}
#[derive(Clone, Debug)]
pub struct RenderTableCell {
colspan: usize,
content: Vec<RenderNode>,
size_estimate: Cell<Option<SizeEstimate>>,
col_width: Option<usize>, }
impl RenderTableCell {
pub fn render<T: Write, D: TextDecorator>(
&mut self,
_renderer: &mut TextRenderer<D>,
_err_out: &mut T,
) {
unimplemented!()
}
pub fn get_size_estimate(&self) -> SizeEstimate {
if self.size_estimate.get().is_none() {
let size = self
.content
.iter()
.map(|node| node.get_size_estimate())
.fold(Default::default(), SizeEstimate::add);
self.size_estimate.set(Some(size));
}
self.size_estimate.get().unwrap()
}
}
#[derive(Clone, Debug)]
pub struct RenderTableRow {
cells: Vec<RenderTableCell>,
col_sizes: Option<Vec<usize>>,
}
impl RenderTableRow {
pub fn cells(&self) -> std::slice::Iter<RenderTableCell> {
self.cells.iter()
}
pub fn cells_mut(&mut self) -> std::slice::IterMut<RenderTableCell> {
self.cells.iter_mut()
}
pub fn num_cells(&self) -> usize {
self.cells.iter().map(|cell| cell.colspan.max(1)).sum()
}
pub fn cell_columns(&mut self) -> Vec<(usize, &mut RenderTableCell)> {
let mut result = Vec::new();
let mut colno = 0;
for cell in &mut self.cells {
let colspan = cell.colspan;
result.push((colno, cell));
colno += colspan;
}
result
}
pub fn into_cells(self, vertical: bool) -> Vec<RenderNode> {
let mut result = Vec::new();
let mut colno = 0;
let col_sizes = self.col_sizes.unwrap();
for mut cell in self.cells {
let colspan = cell.colspan;
let col_width = if vertical {
col_sizes[colno]
} else {
col_sizes[colno..colno + cell.colspan].iter().sum::<usize>()
};
if col_width > 0 {
cell.col_width = Some(col_width + cell.colspan - 1);
result.push(RenderNode::new(RenderNodeInfo::TableCell(cell)));
}
colno += colspan;
}
result
}
}
#[derive(Clone, Debug)]
pub struct RenderTable {
rows: Vec<RenderTableRow>,
num_columns: usize,
size_estimate: Cell<Option<SizeEstimate>>,
}
impl RenderTable {
pub fn new(rows: Vec<RenderTableRow>) -> RenderTable {
let num_columns = rows.iter().map(|r| r.num_cells()).max().unwrap_or(0);
RenderTable {
rows,
num_columns,
size_estimate: Cell::new(None),
}
}
pub fn rows(&self) -> std::slice::Iter<RenderTableRow> {
self.rows.iter()
}
pub fn rows_mut(&mut self) -> std::slice::IterMut<RenderTableRow> {
self.rows.iter_mut()
}
pub fn into_rows(self, col_sizes: Vec<usize>, vert: bool) -> Vec<RenderNode> {
self.rows
.into_iter()
.map(|mut tr| {
tr.col_sizes = Some(col_sizes.clone());
RenderNode::new(RenderNodeInfo::TableRow(tr, vert))
})
.collect()
}
fn calc_size_estimate(&self) {
if self.num_columns == 0 {
self.size_estimate.set(Some(SizeEstimate {
size: 0,
min_width: 0,
}));
return;
}
let mut sizes: Vec<SizeEstimate> = vec![Default::default(); self.num_columns];
for row in self.rows() {
let mut colno = 0usize;
for cell in row.cells() {
let cellsize = cell.get_size_estimate();
for colnum in 0..cell.colspan {
sizes[colno + colnum].size += cellsize.size / cell.colspan;
sizes[colno + colnum].min_width = max(
sizes[colno + colnum].min_width / cell.colspan,
cellsize.min_width,
);
}
colno += cell.colspan;
}
}
let size = sizes.iter().map(|s| s.size).sum(); let min_width = sizes.iter().map(|s| s.min_width).sum::<usize>() + self.num_columns - 1;
self.size_estimate
.set(Some(SizeEstimate { size, min_width }));
}
pub fn get_size_estimate(&self) -> SizeEstimate {
if self.size_estimate.get().is_none() {
self.calc_size_estimate();
}
self.size_estimate.get().unwrap()
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum RenderNodeInfo {
Text(String),
Container(Vec<RenderNode>),
Link(String, Vec<RenderNode>),
Em(Vec<RenderNode>),
Strong(Vec<RenderNode>),
Strikeout(Vec<RenderNode>),
Code(Vec<RenderNode>),
Img(String, String),
Block(Vec<RenderNode>),
Header(usize, Vec<RenderNode>),
Div(Vec<RenderNode>),
Pre(Vec<RenderNode>),
BlockQuote(Vec<RenderNode>),
Ul(Vec<RenderNode>),
Ol(i64, Vec<RenderNode>),
Dl(Vec<RenderNode>),
Dt(Vec<RenderNode>),
Dd(Vec<RenderNode>),
Break,
Table(RenderTable),
TableBody(Vec<RenderTableRow>),
TableRow(RenderTableRow, bool),
TableCell(RenderTableCell),
FragStart(String),
Coloured(Colour, Vec<RenderNode>),
}
#[derive(Clone, Debug)]
pub struct RenderNode {
size_estimate: Cell<Option<SizeEstimate>>,
info: RenderNodeInfo,
}
impl RenderNode {
pub fn new(info: RenderNodeInfo) -> RenderNode {
RenderNode {
size_estimate: Cell::new(None),
info,
}
}
pub fn get_size_estimate(&self) -> SizeEstimate {
if let Some(s) = self.size_estimate.get() {
return s;
};
use RenderNodeInfo::*;
let estimate = match self.info {
Text(ref t) | Img(_, ref t) => {
use unicode_width::UnicodeWidthChar;
let mut len = 0;
let mut in_whitespace = false;
for c in t.trim().chars() {
let is_ws = c.is_whitespace();
if !is_ws {
len += UnicodeWidthChar::width(c).unwrap_or(0);
if in_whitespace {
len += 1;
}
}
in_whitespace = is_ws;
}
if let Some(true) = t.chars().next().map(|c| c.is_whitespace()) {
len += 1;
}
if let Img(_, _) = self.info {
len += 2;
}
SizeEstimate {
size: len,
min_width: len.min(MIN_WIDTH),
}
}
Container(ref v) | Em(ref v) | Strong(ref v) | Strikeout(ref v) | Code(ref v)
| Block(ref v) | Div(ref v) | Pre(ref v) | BlockQuote(ref v) | Dl(ref v)
| Dt(ref v) | Dd(ref v) => v
.iter()
.map(RenderNode::get_size_estimate)
.fold(Default::default(), SizeEstimate::add),
Link(ref _target, ref v) => v
.iter()
.map(RenderNode::get_size_estimate)
.fold(Default::default(), SizeEstimate::add)
.add(SizeEstimate {
size: 5,
min_width: 5,
}),
Ul(ref v) => v
.iter()
.map(RenderNode::get_size_estimate)
.fold(Default::default(), SizeEstimate::add)
.add(SizeEstimate {
size: 2,
min_width: 2,
}),
Ol(i, ref v) => v
.iter()
.map(RenderNode::get_size_estimate)
.fold(Default::default(), SizeEstimate::add)
.add(SizeEstimate {
size: i.to_string().len() + 2,
min_width: i.to_string().len() + 2,
}),
Header(level, ref v) => v
.iter()
.map(RenderNode::get_size_estimate)
.fold(Default::default(), SizeEstimate::add)
.add(SizeEstimate {
size: 0,
min_width: MIN_WIDTH + level + 2,
}),
Break => SizeEstimate {
size: 1,
min_width: 1,
},
Table(ref t) => t.get_size_estimate(),
TableRow(..) | TableBody(_) | TableCell(_) => unimplemented!(),
FragStart(_) => Default::default(),
Coloured(_, ref v) => v
.iter()
.map(RenderNode::get_size_estimate)
.fold(Default::default(), SizeEstimate::add),
};
self.size_estimate.set(Some(estimate));
estimate
}
pub fn is_shallow_empty(&self) -> bool {
use RenderNodeInfo::*;
match self.info {
Text(ref t) | Img(_, ref t) => {
let len = t.trim().len();
len == 0
}
Container(ref v)
| Link(_, ref v)
| Em(ref v)
| Strong(ref v)
| Strikeout(ref v)
| Code(ref v)
| Block(ref v)
| Div(ref v)
| Pre(ref v)
| BlockQuote(ref v)
| Dl(ref v)
| Dt(ref v)
| Dd(ref v)
| Ul(ref v)
| Ol(_, ref v) => v.is_empty(),
Header(_level, ref v) => v.is_empty(),
Break => true,
Table(ref _t) => false,
TableRow(..) | TableBody(_) | TableCell(_) => false,
FragStart(_) => true,
Coloured(_, ref v) => v.is_empty(),
}
}
}
fn precalc_size_estimate<'a>(node: &'a RenderNode) -> Result<TreeMapResult<(), &'a RenderNode, ()>> {
use RenderNodeInfo::*;
if node.size_estimate.get().is_some() {
return Ok(TreeMapResult::Nothing);
}
Ok(match node.info {
Text(_) | Img(_, _) | Break | FragStart(_) => {
let _ = node.get_size_estimate();
TreeMapResult::Nothing
}
Container(ref v)
| Link(_, ref v)
| Em(ref v)
| Strong(ref v)
| Strikeout(ref v)
| Code(ref v)
| Block(ref v)
| Div(ref v)
| Pre(ref v)
| BlockQuote(ref v)
| Ul(ref v)
| Ol(_, ref v)
| Dl(ref v)
| Dt(ref v)
| Dd(ref v)
| Header(_, ref v) => TreeMapResult::PendingChildren {
children: v.iter().collect(),
cons: Box::new(move |_, _cs| {
node.get_size_estimate();
Ok(None)
}),
prefn: None,
postfn: None,
},
Table(ref t) => {
let mut children = Vec::new();
for row in &t.rows {
for cell in &row.cells {
children.extend(cell.content.iter());
}
}
TreeMapResult::PendingChildren {
children,
cons: Box::new(move |_, _cs| {
node.get_size_estimate();
Ok(None)
}),
prefn: None,
postfn: None,
}
}
TableRow(..) | TableBody(_) | TableCell(_) => unimplemented!(),
Coloured(_, ref v) => TreeMapResult::PendingChildren {
children: v.iter().collect(),
cons: Box::new(move |_, _cs| {
node.get_size_estimate();
Ok(None)
}),
prefn: None,
postfn: None,
},
})
}
fn children_to_render_nodes<T: Write>(handle: Handle, err_out: &mut T) -> Result<Vec<RenderNode>> {
handle
.children
.borrow()
.iter()
.flat_map(|ch| dom_to_render_tree(ch.clone(), err_out).transpose())
.collect()
}
fn list_children_to_render_nodes<T: Write>(handle: Handle, err_out: &mut T) -> Result<Vec<RenderNode>> {
let mut children = Vec::new();
for child in handle.children.borrow().iter() {
match child.data {
Element { ref name, .. } => match name.expanded() {
expanded_name!(html "li") => {
let li_children = children_to_render_nodes(child.clone(), err_out)?;
children.push(RenderNode::new(RenderNodeInfo::Block(li_children)));
}
_ => {}
},
Comment { .. } => {}
_ => {
html_trace!("Unhandled in list: {:?}\n", child);
}
}
}
Ok(children)
}
fn desc_list_children_to_render_nodes<T: Write>(
handle: Handle,
err_out: &mut T,
) -> Result<Vec<RenderNode>> {
let mut children = Vec::new();
for child in handle.children.borrow().iter() {
match child.data {
Element { ref name, .. } => match name.expanded() {
expanded_name!(html "dt") => {
let dt_children = children_to_render_nodes(child.clone(), err_out)?;
children.push(RenderNode::new(RenderNodeInfo::Dt(dt_children)));
}
expanded_name!(html "dd") => {
let dd_children = children_to_render_nodes(child.clone(), err_out)?;
children.push(RenderNode::new(RenderNodeInfo::Dd(dd_children)));
}
_ => {}
},
Comment { .. } => {}
_ => {
html_trace!("Unhandled in list: {:?}\n", child);
}
}
}
Ok(children)
}
fn table_to_render_tree<'a, 'b, T: Write>(
handle: Handle,
_err_out: &'b mut T,
) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode> {
pending(handle, |_, rowset| {
let mut rows = vec![];
for bodynode in rowset {
if let RenderNodeInfo::TableBody(body) = bodynode.info {
rows.extend(body);
} else {
html_trace!("Found in table: {:?}", bodynode.info);
}
}
Ok(Some(RenderNode::new(RenderNodeInfo::Table(RenderTable::new(
rows,
)))))
})
}
fn tbody_to_render_tree<'a, 'b, T: Write>(
handle: Handle,
_err_out: &'b mut T,
) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode> {
pending(handle, |_, rowchildren| {
let mut rows = rowchildren
.into_iter()
.flat_map(|rownode| {
if let RenderNodeInfo::TableRow(row, _) = rownode.info {
Some(row)
} else {
html_trace!(" [[tbody child: {:?}]]", rownode);
None
}
})
.collect::<Vec<_>>();
let num_columns =
rows.iter()
.map(|row| {
row.cells()
.map(|cell| (cell.colspan == 0, cell.colspan.max(1)))
.fold((false, 0), |a, b| {
(a.0 || b.0, a.1 + b.1)
})
})
.collect::<Vec<_>>();
let max_columns = num_columns.iter().map(|(_, span)| span).max().unwrap_or(&1);
for (i, &(has_zero, num_cols)) in num_columns.iter().enumerate() {
if has_zero {
for cell in rows[i].cells_mut() {
if cell.colspan == 0 {
cell.colspan = max_columns - num_cols + 1;
}
}
}
}
Ok(Some(RenderNode::new(RenderNodeInfo::TableBody(rows))))
})
}
fn tr_to_render_tree<'a, 'b, T: Write>(
handle: Handle,
_err_out: &'b mut T,
) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode> {
pending(handle, |_, cellnodes| {
let cells = cellnodes
.into_iter()
.flat_map(|cellnode| {
if let RenderNodeInfo::TableCell(cell) = cellnode.info {
Some(cell)
} else {
html_trace!(" [[tr child: {:?}]]", cellnode);
None
}
})
.collect();
Ok(Some(RenderNode::new(RenderNodeInfo::TableRow(
RenderTableRow {
cells,
col_sizes: None,
},
false,
))))
})
}
fn td_to_render_tree<'a, 'b, T: Write>(
handle: Handle,
_err_out: &'b mut T,
) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode> {
let mut colspan = 1;
if let Element { ref attrs, .. } = handle.data {
for attr in attrs.borrow().iter() {
if &attr.name.local == "colspan" {
let v: &str = &*attr.value;
colspan = v.parse().unwrap_or(1);
}
}
}
pending(handle, move |_, children| {
Ok(Some(RenderNode::new(RenderNodeInfo::TableCell(
RenderTableCell {
colspan,
content: children,
size_estimate: Cell::new(None),
col_width: None,
},
))))
})
}
type ResultReducer<'a, C, R> = dyn Fn(&mut C, Vec<R>) -> Result<Option<R>> + 'a;
type ChildPreFn<C, N> = dyn Fn(&mut C, &N) -> Result<()>;
type ChildPostFn<C, R> = dyn Fn(&mut C, &R) -> Result<()>;
enum TreeMapResult<'a, C, N, R> {
Finished(R),
PendingChildren {
children: Vec<N>,
cons: Box<ResultReducer<'a, C, R>>,
prefn: Option<Box<ChildPreFn<C, N>>>,
postfn: Option<Box<ChildPostFn<C, R>>>,
},
Nothing,
}
fn tree_map_reduce<'a, C, N, R, M>(context: &mut C, top: N, mut process_node: M) -> Result<Option<R>>
where
M: for<'c> FnMut(&'c mut C, N) -> Result<TreeMapResult<'a, C, N, R>>,
{
struct PendingNode<'a, C, R, N> {
construct: Box<ResultReducer<'a, C, R>>,
prefn: Option<Box<ChildPreFn<C, N>>>,
postfn: Option<Box<ChildPostFn<C, R>>>,
children: Vec<R>,
to_process: std::vec::IntoIter<N>,
}
let mut pending_stack = vec![PendingNode {
construct: Box::new(|_, mut cs| Ok(cs.pop())),
prefn: None,
postfn: None,
children: Vec::new(),
to_process: vec![top].into_iter(),
}];
loop {
let next_node = pending_stack.last_mut().unwrap().to_process.next();
if let Some(h) = next_node {
pending_stack
.last_mut()
.unwrap()
.prefn
.as_ref()
.map(|ref f| f(context, &h))
.transpose()?;
match process_node(context, h)? {
TreeMapResult::Finished(result) => {
pending_stack
.last_mut()
.unwrap()
.postfn
.as_ref()
.map(|ref f| f(context, &result));
pending_stack.last_mut().unwrap().children.push(result);
}
TreeMapResult::PendingChildren {
children,
cons,
prefn,
postfn,
} => {
pending_stack.push(PendingNode {
construct: cons,
prefn,
postfn,
children: Vec::new(),
to_process: children.into_iter(),
});
}
TreeMapResult::Nothing => {}
};
} else {
let completed = pending_stack.pop().unwrap();
let reduced = (completed.construct)(context, completed.children)?;
if let Some(node) = reduced {
if let Some(parent) = pending_stack.last_mut() {
parent.postfn.as_ref().map(|ref f| f(context, &node));
parent.children.push(node);
} else {
break Ok(Some(node));
}
} else {
if pending_stack.is_empty() {
break Ok(None);
}
}
}
}
}
#[derive(Default, Debug)]
struct HtmlContext {
#[cfg(feature = "css")]
style_data: css::StyleData,
#[cfg(feature = "css")]
use_doc_css: bool,
max_wrap_width: Option<usize>,
}
fn dom_to_render_tree_with_context<T: Write>(
handle: Handle,
err_out: &mut T,
context: &mut HtmlContext)
-> Result<Option<RenderNode>> {
html_trace!("### dom_to_render_tree: HTML: {:?}", handle);
#[cfg(feature = "css")]
if context.use_doc_css {
let mut doc_style_data = css::dom_to_stylesheet(handle.clone(), err_out)?;
doc_style_data.merge(std::mem::take(&mut context.style_data));
context.style_data = doc_style_data;
}
let result = tree_map_reduce(context, handle, |context, handle| {
process_dom_node(handle, err_out, context)
});
html_trace!("### dom_to_render_tree: out= {:#?}", result);
result
}
pub fn dom_to_render_tree<T: Write>(handle: Handle, err_out: &mut T) -> Result<Option<RenderNode>> {
dom_to_render_tree_with_context(handle, err_out, &mut Default::default())
}
fn pending<'a, F>(handle: Handle, f: F) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode>
where
for<'r> F: Fn(&'r mut HtmlContext, std::vec::Vec<RenderNode>) -> Result<Option<RenderNode>> + 'static,
{
TreeMapResult::PendingChildren {
children: handle.children.borrow().clone(),
cons: Box::new(f),
prefn: None,
postfn: None,
}
}
fn prepend_marker(prefix: RenderNode, mut orig: RenderNode) -> RenderNode {
use RenderNodeInfo::*;
html_trace!("prepend_marker({:?}, {:?})", prefix, orig);
match orig.info {
Block(ref mut children)
| Div(ref mut children)
| Pre(ref mut children)
| BlockQuote(ref mut children)
| Container(ref mut children)
| TableCell(RenderTableCell {
content: ref mut children,
..
}) => {
children.insert(0, prefix);
}
TableRow(ref mut rrow, _) => {
if rrow.cells.len() > 0 {
rrow.cells[0].content.insert(0, prefix);
}
}
TableBody(ref mut rows) | Table(RenderTable { ref mut rows, .. }) => {
if rows.len() > 0 {
let rrow = &mut rows[0];
if rrow.cells.len() > 0 {
rrow.cells[0].content.insert(0, prefix);
}
}
}
_ => {
let result = RenderNode::new(Container(vec![prefix, orig]));
html_trace!("prepend_marker() -> {:?}", result);
return result;
}
}
html_trace!("prepend_marker() -> {:?}", &orig);
orig
}
fn process_dom_node<'a, 'b, 'c, T: Write>(
handle: Handle,
err_out: &'b mut T,
#[allow(unused)] context: &'c mut HtmlContext,
) -> Result<TreeMapResult<'a, HtmlContext, Handle, RenderNode>> {
use RenderNodeInfo::*;
use TreeMapResult::*;
Ok(match handle.clone().data {
Document => pending(handle, |_context, cs| Ok(Some(RenderNode::new(Container(cs))))),
Comment { .. } => Nothing,
Element {
ref name,
ref attrs,
..
} => {
let mut frag_from_name_attr = false;
#[cfg(feature = "css")]
let mut css_colour = None;
#[cfg(feature = "css")]
{
for style in context.style_data.matching_rules(&handle) {
match style {
css::Style::Colour(col) => {
css_colour = Some(col);
}
css::Style::DisplayNone => {
return Ok(Nothing);
}
}
}
};
let result = match name.expanded() {
expanded_name!(html "html")
| expanded_name!(html "body") => {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
}
expanded_name!(html "link")
| expanded_name!(html "meta")
| expanded_name!(html "hr")
| expanded_name!(html "script")
| expanded_name!(html "style")
| expanded_name!(html "head") => {
Nothing
}
expanded_name!(html "span") => {
#[cfg(not(feature = "css"))]
{
pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
}
#[cfg(feature = "css")]
{
if let Some(Ok(colour)) = css_colour.as_ref().map(TryFrom::try_from) {
pending(handle, move |_, cs| Ok(Some(RenderNode::new(Coloured(colour, vec![RenderNode::new(Container(cs))])))))
} else {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
}
}
}
#[cfg(feature = "css")]
expanded_name!(html "font") => {
let mut colour = None;
if context.use_doc_css {
use lightningcss::traits::Parse;
let borrowed = attrs.borrow();
for attr in borrowed.iter() {
if &attr.name.local == "color" {
colour = CssColor::parse_string(&*attr.value)
.ok()
.and_then(|c| TryFrom::try_from(&c).ok());
break;
}
}
}
if let Some(colour) = colour {
pending(handle, move |_, cs| Ok(Some(RenderNode::new(Coloured(colour, vec![RenderNode::new(Container(cs))])))))
} else {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
}
}
expanded_name!(html "a") => {
let borrowed = attrs.borrow();
let mut target = None;
frag_from_name_attr = true;
for attr in borrowed.iter() {
if &attr.name.local == "href" {
target = Some(&*attr.value);
break;
}
}
PendingChildren {
children: handle.children.borrow().clone(),
cons: if let Some(href) = target {
let href: String = href.into();
Box::new(move |_, cs: Vec<RenderNode>| {
if cs.iter().any(|c| !c.is_shallow_empty()) {
Ok(Some(RenderNode::new(Link(href.clone(), cs))))
} else {
Ok(None)
}
})
} else {
Box::new(|_, cs| Ok(Some(RenderNode::new(Container(cs)))))
},
prefn: None,
postfn: None,
}
}
expanded_name!(html "em") => pending(handle, |_, cs| Ok(Some(RenderNode::new(Em(cs))))),
expanded_name!(html "strong") => {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Strong(cs)))))
}
expanded_name!(html "s") => {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Strikeout(cs)))))
}
expanded_name!(html "code") => {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Code(cs)))))
}
expanded_name!(html "img") => {
let borrowed = attrs.borrow();
let mut title = None;
let mut src = None;
for attr in borrowed.iter() {
if &attr.name.local == "alt" && !attr.value.is_empty() {
title = Some(&*attr.value);
}
if &attr.name.local == "src" && !attr.value.is_empty() {
src = Some(&*attr.value);
}
if title.is_some() && src.is_some() {
break;
}
}
if let (Some(title), Some(src)) = (title, src) {
Finished(RenderNode::new(Img(src.into(), title.into())))
} else {
Nothing
}
}
expanded_name!(html "h1")
| expanded_name!(html "h2")
| expanded_name!(html "h3")
| expanded_name!(html "h4") => {
let level: usize = name.local[1..].parse().unwrap();
pending(handle, move |_, cs| {
Ok(Some(RenderNode::new(Header(level, cs))))
})
}
expanded_name!(html "p") => {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Block(cs)))))
}
expanded_name!(html "div") => {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Div(cs)))))
}
expanded_name!(html "pre") => {
pending(handle, |_, cs| Ok(Some(RenderNode::new(Pre(cs)))))
}
expanded_name!(html "br") => Finished(RenderNode::new(Break)),
expanded_name!(html "table") => table_to_render_tree(handle.clone(), err_out),
expanded_name!(html "thead") | expanded_name!(html "tbody") => {
tbody_to_render_tree(handle.clone(), err_out)
}
expanded_name!(html "tr") => tr_to_render_tree(handle.clone(), err_out),
expanded_name!(html "th") | expanded_name!(html "td") => {
td_to_render_tree(handle.clone(), err_out)
}
expanded_name!(html "blockquote") => {
pending(handle, |_, cs| Ok(Some(RenderNode::new(BlockQuote(cs)))))
}
expanded_name!(html "ul") => Finished(RenderNode::new(Ul(
list_children_to_render_nodes(handle.clone(), err_out)?,
))),
expanded_name!(html "ol") => {
let borrowed = attrs.borrow();
let mut start = 1;
for attr in borrowed.iter() {
if &attr.name.local == "start" {
start = attr.value.parse().ok().unwrap_or(1);
break;
}
}
Finished(RenderNode::new(Ol(
start,
list_children_to_render_nodes(handle.clone(), err_out)?,
)))
}
expanded_name!(html "dl") => Finished(RenderNode::new(Dl(
desc_list_children_to_render_nodes(handle.clone(), err_out)?,
))),
_ => {
html_trace!("Unhandled element: {:?}\n", name.local);
pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
}
};
let mut fragment = None;
let borrowed = attrs.borrow();
for attr in borrowed.iter() {
if &attr.name.local == "id" || (frag_from_name_attr && &attr.name.local == "name") {
fragment = Some(attr.value.to_string());
break;
}
}
if let Some(fragname) = fragment {
match result {
Finished(node) => {
Finished(prepend_marker(RenderNode::new(FragStart(fragname)), node))
}
Nothing => Finished(RenderNode::new(FragStart(fragname))),
PendingChildren {
children,
cons,
prefn,
postfn,
} => {
let fragname: String = fragname.into();
PendingChildren {
children,
prefn,
postfn,
cons: Box::new(move |ctx, ch| {
let fragnode = RenderNode::new(FragStart(fragname.clone()));
match cons(ctx, ch)? {
None => Ok(Some(fragnode)),
Some(node) => Ok(Some(prepend_marker(fragnode, node))),
}
}),
}
}
}
} else {
result
}
}
markup5ever_rcdom::NodeData::Text { contents: ref tstr } => {
Finished(RenderNode::new(Text((&*tstr.borrow()).into())))
}
_ => {
write!(err_out, "Unhandled node type.\n").unwrap();
Nothing
}
})
}
fn render_tree_to_string<T: Write, D: TextDecorator>(
renderer: SubRenderer<D>,
tree: RenderNode,
err_out: &mut T,
) -> Result<SubRenderer<D>> {
tree_map_reduce(&mut (), &tree, |_, node| precalc_size_estimate(&node))?;
let mut renderer = TextRenderer::new(renderer);
tree_map_reduce(&mut renderer, tree, |renderer, node| {
do_render_node(renderer, node, err_out)
})?;
let (mut renderer, links) = renderer.into_inner();
let lines = renderer.finalise(links);
if !lines.is_empty() {
renderer.start_block()?;
renderer.fmt_links(lines);
}
Ok(renderer)
}
fn pending2<
'a,
D: TextDecorator,
F: Fn(&mut TextRenderer<D>, Vec<Option<SubRenderer<D>>>) -> Result<Option<Option<SubRenderer<D>>>>
+ 'static,
>(
children: Vec<RenderNode>,
f: F,
) -> TreeMapResult<'a, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
TreeMapResult::PendingChildren {
children,
cons: Box::new(f),
prefn: None,
postfn: None,
}
}
fn do_render_node<'a, 'b, T: Write, D: TextDecorator>(
renderer: &mut TextRenderer<D>,
tree: RenderNode,
err_out: &'b mut T,
) -> Result<TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>>> {
html_trace!("do_render_node({:?}", tree);
use RenderNodeInfo::*;
use TreeMapResult::*;
Ok(match tree.info {
Text(ref tstr) => {
renderer.add_inline_text(tstr)?;
Finished(None)
}
Container(children) => pending2(children, |_, _| Ok(Some(None))),
Link(href, children) => {
renderer.start_link(&href)?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_link()?;
Ok(Some(None))
})
}
Em(children) => {
renderer.start_emphasis()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_emphasis()?;
Ok(Some(None))
})
}
Strong(children) => {
renderer.start_strong()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_strong()?;
Ok(Some(None))
})
}
Strikeout(children) => {
renderer.start_strikeout()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_strikeout()?;
Ok(Some(None))
})
}
Code(children) => {
renderer.start_code()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_code()?;
Ok(Some(None))
})
}
Img(src, title) => {
renderer.add_image(&src, &title)?;
Finished(None)
}
Block(children) => {
renderer.start_block()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_block();
Ok(Some(None))
})
}
Header(level, children) => {
let prefix = renderer.header_prefix(level);
let sub_builder = renderer.new_sub_renderer(renderer.width().checked_sub(prefix.len()).ok_or(Error::TooNarrow)?)?;
renderer.push(sub_builder);
pending2(children, move |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.pop();
renderer.start_block()?;
renderer.append_subrender(sub_builder, repeat(&prefix[..]))?;
renderer.end_block();
Ok(Some(None))
})
}
Div(children) => {
renderer.new_line()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.new_line()?;
Ok(Some(None))
})
}
Pre(children) => {
renderer.new_line()?;
renderer.start_pre();
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.new_line()?;
renderer.end_pre();
Ok(Some(None))
})
}
BlockQuote(children) => {
let prefix = renderer.quote_prefix();
let sub_builder = renderer.new_sub_renderer(renderer.width().checked_sub(prefix.len()).ok_or(Error::TooNarrow)?)?;
renderer.push(sub_builder);
pending2(children, move |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.pop();
renderer.start_block()?;
renderer.append_subrender(sub_builder, repeat(&prefix[..]))?;
renderer.end_block();
Ok(Some(None))
})
}
Ul(items) => {
renderer.start_block()?;
let prefix = renderer.unordered_item_prefix();
let prefix_len = prefix.len();
TreeMapResult::PendingChildren {
children: items,
cons: Box::new(|_, _| Ok(Some(None))),
prefn: Some(Box::new(move |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.new_sub_renderer(renderer.width().checked_sub(prefix_len).ok_or(Error::TooNarrow)?)?;
renderer.push(sub_builder);
Ok(())
})),
postfn: Some(Box::new(move |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.pop();
let indent = " ".repeat(prefix.len());
renderer.append_subrender(
sub_builder,
once(&prefix[..]).chain(repeat(&indent[..])),
)?;
Ok(())
})),
}
}
Ol(start, items) => {
renderer.start_block()?;
let num_items = items.len();
let min_number = start;
let max_number = start + (num_items as i64) - 1;
let prefix_width_min = renderer.ordered_item_prefix(min_number).len();
let prefix_width_max = renderer.ordered_item_prefix(max_number).len();
let prefix_width = max(prefix_width_min, prefix_width_max);
let prefixn = format!("{: <width$}", "", width = prefix_width);
let i: Cell<_> = Cell::new(start);
TreeMapResult::PendingChildren {
children: items,
cons: Box::new(|_, _| Ok(Some(None))),
prefn: Some(Box::new(move |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.new_sub_renderer(renderer.width().checked_sub(prefix_width).ok_or(Error::TooNarrow)?)?;
renderer.push(sub_builder);
Ok(())
})),
postfn: Some(Box::new(move |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.pop();
let prefix1 = renderer.ordered_item_prefix(i.get());
let prefix1 = format!("{: <width$}", prefix1, width = prefix_width);
renderer.append_subrender(
sub_builder,
once(prefix1.as_str()).chain(repeat(prefixn.as_str())),
)?;
i.set(i.get() + 1);
Ok(())
})),
}
}
Dl(items) => {
renderer.start_block()?;
TreeMapResult::PendingChildren {
children: items,
cons: Box::new(|_, _| Ok(Some(None))),
prefn: None,
postfn: None,
}
}
Dt(children) => {
renderer.new_line()?;
renderer.start_emphasis()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_emphasis()?;
Ok(Some(None))
})
}
Dd(children) => {
let sub_builder = renderer.new_sub_renderer(renderer.width().checked_sub(2).ok_or(Error::TooNarrow)?)?;
renderer.push(sub_builder);
pending2(children, |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.pop();
renderer.append_subrender(sub_builder, repeat(" "))?;
Ok(Some(None))
})
}
Break => {
renderer.new_line_hard()?;
Finished(None)
}
Table(tab) => render_table_tree(renderer, tab, err_out)?,
TableRow(row, false) => render_table_row(renderer, row, err_out),
TableRow(row, true) => render_table_row_vert(renderer, row, err_out),
TableBody(_) => unimplemented!("Unexpected TableBody while rendering"),
TableCell(cell) => render_table_cell(renderer, cell, err_out),
FragStart(fragname) => {
renderer.record_frag_start(&fragname);
Finished(None)
}
Coloured(colour, children) => {
renderer.push_colour(colour);
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.pop_colour();
Ok(Some(None))
})
}
})
}
fn render_table_tree<T: Write, D: TextDecorator>(
renderer: &mut TextRenderer<D>,
table: RenderTable,
_err_out: &mut T,
) -> Result<TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>>> {
let num_columns = table.num_columns;
let mut col_sizes: Vec<SizeEstimate> = vec![Default::default(); num_columns];
for row in table.rows() {
let mut colno = 0;
for cell in row.cells() {
let mut estimate = cell.get_size_estimate();
estimate.size /= cell.colspan;
estimate.min_width /= cell.colspan;
for i in 0..cell.colspan {
col_sizes[colno + i] = (col_sizes[colno + i]).max(estimate);
}
colno += cell.colspan;
}
}
let tot_size: usize = col_sizes.iter().map(|est| est.size).sum();
let min_size: usize = col_sizes.iter().map(|est| est.min_width).sum::<usize>()
+ col_sizes.len().saturating_sub(1);
let width = renderer.width();
let vert_row = min_size > width || width == 0;
let mut col_widths: Vec<usize> = if !vert_row {
col_sizes
.iter()
.map(|sz| {
if sz.size == 0 {
0
} else {
min(
sz.size,
if usize::MAX / width <= sz.size {
max((width / tot_size) * sz.size, sz.min_width)
} else {
max(sz.size * width / tot_size, sz.min_width)
},
)
}
})
.collect()
} else {
col_sizes.iter().map(|_| width).collect()
};
if !vert_row {
let num_cols = col_widths.len();
if num_cols > 0 {
loop {
let cur_width = col_widths.iter().cloned().sum::<usize>() + num_cols - 1;
if cur_width <= width {
break;
}
let (i, _) = col_widths
.iter()
.cloned()
.enumerate()
.max_by_key(|&(colno, width)| {
(
width.saturating_sub(col_sizes[colno].min_width),
width,
usize::max_value() - colno,
)
})
.unwrap();
col_widths[i] -= 1;
}
}
}
renderer.start_block()?;
let table_width = if vert_row {
width
} else {
col_widths.iter().cloned().sum::<usize>()
+ col_widths
.iter()
.filter(|&w| w > &0)
.count()
.saturating_sub(1)
};
renderer.add_horizontal_border_width(table_width)?;
Ok(TreeMapResult::PendingChildren {
children: table.into_rows(col_widths, vert_row),
cons: Box::new(|_, _| Ok(Some(None))),
prefn: Some(Box::new(|_, _| {Ok(())})),
postfn: Some(Box::new(|_, _| {Ok(())})),
})
}
fn render_table_row<T: Write, D: TextDecorator>(
_renderer: &mut TextRenderer<D>,
row: RenderTableRow,
_err_out: &mut T,
) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
TreeMapResult::PendingChildren {
children: row.into_cells(false),
cons: Box::new(|builders, children| {
let children: Vec<_> = children.into_iter().map(Option::unwrap).collect();
if children.iter().any(|c| !c.empty()) {
builders.append_columns_with_borders(children, true)?;
}
Ok(Some(None))
}),
prefn: Some(Box::new(|renderer: &mut TextRenderer<D>, node| {
if let RenderNodeInfo::TableCell(ref cell) = node.info {
let sub_builder = renderer.new_sub_renderer(cell.col_width.unwrap())?;
renderer.push(sub_builder);
Ok(())
} else {
panic!()
}
})),
postfn: Some(Box::new(|_renderer: &mut TextRenderer<D>, _| {Ok(())})),
}
}
fn render_table_row_vert<T: Write, D: TextDecorator>(
_renderer: &mut TextRenderer<D>,
row: RenderTableRow,
_err_out: &mut T,
) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
TreeMapResult::PendingChildren {
children: row.into_cells(true),
cons: Box::new(|builders, children| {
let children: Vec<_> = children.into_iter().map(Option::unwrap).collect();
builders.append_vert_row(children)?;
Ok(Some(None))
}),
prefn: Some(Box::new(|renderer: &mut TextRenderer<D>, node| {
if let RenderNodeInfo::TableCell(ref cell) = node.info {
let sub_builder = renderer.new_sub_renderer(cell.col_width.unwrap())?;
renderer.push(sub_builder);
Ok(())
} else {
Err(Error::Fail)
}
})),
postfn: Some(Box::new(|_renderer: &mut TextRenderer<D>, _| {Ok(())})),
}
}
fn render_table_cell<T: Write, D: TextDecorator>(
_renderer: &mut TextRenderer<D>,
cell: RenderTableCell,
_err_out: &mut T,
) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
pending2(cell.content, |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.pop();
Ok(Some(Some(sub_builder)))
})
}
pub mod config {
use crate::{render::text_renderer::{
PlainDecorator, RichDecorator, TaggedLine, TextDecorator, RichAnnotation
}, Result, RenderTree, HtmlContext};
#[cfg(feature = "css")]
use crate::css::StyleData;
pub struct Config<D: TextDecorator> {
decorator: D,
max_wrap_width: Option<usize>,
#[cfg(feature = "css")]
style: StyleData,
#[cfg(feature = "css")]
use_doc_css: bool,
}
impl<D: TextDecorator> Config<D> {
fn make_context(&mut self) -> HtmlContext {
HtmlContext {
#[cfg(feature = "css")]
style_data: std::mem::take(&mut self.style),
#[cfg(feature = "css")]
use_doc_css: self.use_doc_css,
max_wrap_width: self.max_wrap_width,
}
}
fn do_parse<R: std::io::Read>(&mut self, context: &mut HtmlContext, input: R) -> Result<RenderTree> {
super::parse_with_context(
input,
context)
}
pub fn string_from_read<R: std::io::Read>(mut self, input: R, width: usize) -> Result<String> {
let mut context = self.make_context();
Ok(self.do_parse(&mut context, input)?
.render_with_context(&mut context, width, self.decorator)?
.into_string()?)
}
pub fn lines_from_read<R: std::io::Read>(mut self, input: R, width: usize) -> Result<Vec<TaggedLine<Vec<D::Annotation>>>> {
let mut context = self.make_context();
Ok(self.do_parse(&mut context, input)?
.render(width, self.decorator)?
.into_lines()?)
}
#[cfg(feature = "css")]
pub fn add_css(mut self, css: &str) -> Result<Self> {
self.style.add_css(css)?;
Ok(self)
}
#[cfg(feature = "css")]
pub fn use_doc_css(mut self) -> Self {
self.use_doc_css = true;
self
}
pub fn max_wrap_width(mut self, wrap_width: usize) -> Self {
self.max_wrap_width = Some(wrap_width);
self
}
}
impl Config<RichDecorator> {
pub fn coloured<R, FMap>(
mut self,
input: R,
width: usize,
colour_map: FMap,
) -> Result<String>
where
R: std::io::Read,
FMap: Fn(&[RichAnnotation], &str) -> String,
{
use std::fmt::Write;
let mut context = self.make_context();
let lines = self.do_parse(&mut context, input)?
.render(width, self.decorator)?
.into_lines()?;
let mut result = String::new();
for line in lines {
for ts in line.tagged_strings() {
write!(result, "{}", colour_map(&ts.tag, &ts.s))?;
}
result.push('\n');
}
Ok(result)
}
}
pub fn rich() -> Config<RichDecorator> {
Config {
decorator: RichDecorator::new(),
#[cfg(feature = "css")]
style: Default::default(),
#[cfg(feature = "css")]
use_doc_css: false,
max_wrap_width: None,
}
}
pub fn plain() -> Config<PlainDecorator> {
Config {
decorator: PlainDecorator::new(),
#[cfg(feature = "css")]
style: Default::default(),
#[cfg(feature = "css")]
use_doc_css: false,
max_wrap_width: None,
}
}
pub fn with_decorator<D: TextDecorator>(decorator: D) -> Config<D> {
Config {
decorator,
#[cfg(feature = "css")]
style: Default::default(),
#[cfg(feature = "css")]
use_doc_css: false,
max_wrap_width: None,
}
}
}
#[derive(Clone, Debug)]
pub struct RenderTree(RenderNode);
impl RenderTree {
fn render_with_context<D: TextDecorator>(self, context: &mut HtmlContext, width: usize, decorator: D) -> Result<RenderedText<D>> {
if width == 0 {
return Err(Error::TooNarrow);
}
let builder = SubRenderer::new(width, context.max_wrap_width, decorator);
let builder = render_tree_to_string(builder, self.0, &mut Discard {})?;
Ok(RenderedText(builder))
}
pub fn render<D: TextDecorator>(self, width: usize, decorator: D) -> Result<RenderedText<D>> {
self.render_with_context(&mut Default::default(), width, decorator)
}
pub fn render_plain(self, width: usize) -> Result<RenderedText<PlainDecorator>> {
self.render(width, PlainDecorator::new())
}
pub fn render_rich(self, width: usize) -> Result<RenderedText<RichDecorator>> {
self.render(width, RichDecorator::new())
}
}
pub struct RenderedText<D: TextDecorator>(SubRenderer<D>);
impl<D: TextDecorator> RenderedText<D> {
pub fn into_string(self) -> Result<String> {
self.0.into_string()
}
pub fn into_lines(self) -> Result<Vec<TaggedLine<Vec<D::Annotation>>>> {
Ok(self.0
.into_lines()?
.into_iter()
.map(RenderLine::into_tagged_line)
.collect())
}
}
fn parse_with_context(mut input: impl io::Read,
context: &mut HtmlContext,
) -> Result<RenderTree> {
let opts = ParseOpts {
tree_builder: TreeBuilderOpts {
drop_doctype: true,
..Default::default()
},
..Default::default()
};
let dom = parse_document(RcDom::default(), opts)
.from_utf8()
.read_from(&mut input)
.unwrap();
let render_tree = dom_to_render_tree_with_context(dom.document.clone(), &mut Discard {}, context)?
.ok_or(Error::Fail)?;
Ok(RenderTree(render_tree))
}
pub fn parse(input: impl io::Read) -> Result<RenderTree> {
parse_with_context(input, &mut Default::default())
}
pub fn from_read_with_decorator<R, D>(input: R, width: usize, decorator: D) -> String
where
R: io::Read,
D: TextDecorator,
{
config::with_decorator(decorator)
.string_from_read(input, width)
.expect("Failed to convert to HTML")
}
pub fn from_read<R>(input: R, width: usize) -> String
where
R: io::Read,
{
config::plain()
.string_from_read(input, width)
.expect("Failed to convert to HTML")
}
pub fn from_read_rich<R>(input: R, width: usize) -> Vec<TaggedLine<Vec<RichAnnotation>>>
where
R: io::Read,
{
config::rich()
.lines_from_read(input, width)
.expect("Failed to convert to HTML")
}
mod ansi_colours;
pub use ansi_colours::from_read_coloured;
#[cfg(test)]
mod tests;