#![deny(missing_docs)]
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadMe;
#[macro_use]
mod macros;
pub mod css;
pub mod render;
trait WhitespaceExt {
fn always_takes_space(&self) -> bool;
fn is_wordbreak_point(&self) -> bool;
}
impl WhitespaceExt for char {
fn always_takes_space(&self) -> bool {
match *self {
'\u{A0}' => true,
c if !c.is_whitespace() => true,
_ => false,
}
}
fn is_wordbreak_point(&self) -> bool {
match *self {
'\u{00A0}' => false,
'\u{200b}' => true,
c if c.is_whitespace() => true,
_ => false,
}
}
}
trait StrExt {
fn trim_collapsible_ws(&self) -> &str;
}
impl StrExt for str {
fn trim_collapsible_ws(&self) -> &str {
self.trim_matches(|c: char| !c.always_takes_space())
}
}
#[cfg(feature = "css_ext")]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct TextStyle {
pub fg_colour: Colour,
pub bg_colour: Option<Colour>,
}
#[cfg(feature = "css_ext")]
impl TextStyle {
pub fn colours(fg_colour: Colour, bg_colour: Colour) -> Self {
TextStyle {
fg_colour,
bg_colour: Some(bg_colour),
}
}
pub fn foreground(fg_colour: Colour) -> Self {
TextStyle {
fg_colour,
bg_colour: None,
}
}
}
#[cfg(feature = "css_ext")]
pub type SyntaxHighlighter = Box<dyn for<'a> Fn(&'a str) -> Vec<(TextStyle, &'a str)>>;
use markup5ever_rcdom::Node;
use render::text_renderer::{
RenderLine, RenderOptions, RichAnnotation, SubRenderer, TaggedLine, TextRenderer,
};
use render::{Renderer, TextDecorator, TrivialDecorator};
use html5ever::driver::ParseOpts;
use html5ever::parse_document;
use html5ever::tree_builder::TreeBuilderOpts;
mod markup5ever_rcdom;
pub use html5ever::{expanded_name, local_name, namespace_url, ns};
pub use markup5ever_rcdom::{
Handle,
NodeData::{Comment, Document, Element},
RcDom,
};
use std::cell::{Cell, RefCell};
use std::cmp::{max, min};
use std::collections::{BTreeSet, HashMap};
#[cfg(feature = "css_ext")]
use std::ops::Range;
use std::rc::Rc;
use unicode_width::UnicodeWidthStr;
use std::io;
use std::io::Write;
use std::iter::{once, repeat};
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
pub(crate) enum WhiteSpace {
#[default]
Normal,
Pre,
#[allow(unused)]
PreWrap,
}
impl WhiteSpace {
pub fn preserve_whitespace(&self) -> bool {
match self {
WhiteSpace::Normal => false,
WhiteSpace::Pre | WhiteSpace::PreWrap => true,
}
}
#[allow(unused)]
pub fn do_wrap(&self) -> bool {
match self {
WhiteSpace::Normal | WhiteSpace::PreWrap => true,
WhiteSpace::Pre => false,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Colour {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl std::fmt::Display for Colour {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd)]
pub(crate) enum StyleOrigin {
#[default]
None,
Agent,
#[allow(unused)]
User,
#[allow(unused)]
Author,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub(crate) struct Specificity {
inline: bool,
id: u16,
class: u16,
typ: u16,
}
impl Specificity {
#[cfg(feature = "css")]
fn inline() -> Self {
Specificity {
inline: true,
id: 0,
class: 0,
typ: 0,
}
}
}
impl std::ops::Add<&Specificity> for &Specificity {
type Output = Specificity;
fn add(self, rhs: &Specificity) -> Self::Output {
Specificity {
inline: self.inline || rhs.inline,
id: self.id + rhs.id,
class: self.class + rhs.class,
typ: self.typ + rhs.typ,
}
}
}
impl std::ops::AddAssign<&Specificity> for Specificity {
fn add_assign(&mut self, rhs: &Specificity) {
self.inline = self.inline || rhs.inline;
self.id += rhs.id;
self.class += rhs.class;
self.typ += rhs.typ;
}
}
impl PartialOrd for Specificity {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match self.inline.partial_cmp(&other.inline) {
Some(core::cmp::Ordering::Equal) => {}
ord => return ord,
}
match self.id.partial_cmp(&other.id) {
Some(core::cmp::Ordering::Equal) => {}
ord => return ord,
}
match self.class.partial_cmp(&other.class) {
Some(core::cmp::Ordering::Equal) => {}
ord => return ord,
}
self.typ.partial_cmp(&other.typ)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct WithSpec<T> {
val: Option<T>,
origin: StyleOrigin,
specificity: Specificity,
important: bool,
}
impl<T: Clone> WithSpec<T> {
pub(crate) fn maybe_update(
&mut self,
important: bool,
origin: StyleOrigin,
specificity: Specificity,
val: T,
) {
if self.val.is_some() {
if self.important && !important {
return;
}
{
use StyleOrigin::*;
match (self.origin, origin) {
(Agent, Agent) | (User, User) | (Author, Author) => {
}
(mine, theirs) => {
if (important && theirs > mine) || (!important && mine > theirs) {
return;
}
}
}
}
if specificity < self.specificity {
return;
}
}
self.val = Some(val);
self.origin = origin;
self.specificity = specificity;
self.important = important;
}
pub fn val(&self) -> Option<&T> {
self.val.as_ref()
}
}
impl<T> Default for WithSpec<T> {
fn default() -> Self {
WithSpec {
val: None,
origin: StyleOrigin::None,
specificity: Default::default(),
important: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ComputedStyle {
#[cfg(feature = "css")]
pub(crate) colour: WithSpec<Colour>,
#[cfg(feature = "css")]
pub(crate) bg_colour: WithSpec<Colour>,
#[cfg(feature = "css")]
pub(crate) display: WithSpec<css::Display>,
pub(crate) white_space: WithSpec<WhiteSpace>,
pub(crate) content: WithSpec<css::PseudoContent>,
#[cfg(feature = "css_ext")]
pub(crate) syntax: WithSpec<css::SyntaxInfo>,
pub(crate) content_before: Option<Box<ComputedStyle>>,
pub(crate) content_after: Option<Box<ComputedStyle>>,
pub(crate) internal_pre: bool,
}
impl ComputedStyle {
pub(crate) fn inherit(&self) -> Self {
self.clone()
}
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("Output width not wide enough.")]
TooNarrow,
#[error("Invalid CSS")]
CssParseError,
#[error("Unknown failure")]
Fail,
#[error("I/O error")]
IoError(#[from] io::Error),
}
impl PartialEq for Error {
fn eq(&self, other: &Error) -> bool {
use Error::*;
match (self, other) {
(TooNarrow, TooNarrow) => true,
#[cfg(feature = "css")]
(CssParseError, CssParseError) => true,
(Fail, Fail) => true,
_ => false,
}
}
}
impl Eq for Error {}
type Result<T> = std::result::Result<T, Error>;
const MIN_WIDTH: usize = 3;
#[derive(Debug, Copy, Clone, Default)]
struct SizeEstimate {
size: usize, min_width: usize,
prefix_size: usize,
}
impl SizeEstimate {
fn add(self, other: SizeEstimate) -> SizeEstimate {
let min_width = max(self.min_width, other.min_width);
SizeEstimate {
size: self.size + other.size,
min_width,
prefix_size: 0,
}
}
fn add_hor(self, other: SizeEstimate) -> SizeEstimate {
SizeEstimate {
size: self.size + other.size,
min_width: self.min_width + other.min_width,
prefix_size: 0,
}
}
fn max(self, other: SizeEstimate) -> SizeEstimate {
SizeEstimate {
size: max(self.size, other.size),
min_width: max(self.min_width, other.min_width),
prefix_size: 0,
}
}
}
#[derive(Clone, Debug)]
struct RenderTableCell {
colspan: usize,
rowspan: usize,
content: Vec<RenderNode>,
size_estimate: Cell<Option<SizeEstimate>>,
col_width: Option<usize>, x_pos: Option<usize>, style: ComputedStyle,
is_dummy: bool,
}
impl RenderTableCell {
fn get_size_estimate(&self) -> SizeEstimate {
let Some(size) = self.size_estimate.get() else {
let size = self
.content
.iter()
.map(|node| node.get_size_estimate())
.fold(Default::default(), SizeEstimate::add);
self.size_estimate.set(Some(size));
return size;
};
size
}
pub fn dummy(colspan: usize) -> Self {
RenderTableCell {
colspan,
rowspan: 1,
content: Default::default(),
size_estimate: Cell::new(Some(SizeEstimate::default())),
col_width: None,
x_pos: None,
style: Default::default(),
is_dummy: true,
}
}
}
#[derive(Clone, Debug)]
struct RenderTableRow {
cells: Vec<RenderTableCell>,
col_sizes: Option<Vec<usize>>,
style: ComputedStyle,
}
impl RenderTableRow {
fn cells(&self) -> std::slice::Iter<'_, RenderTableCell> {
self.cells.iter()
}
fn cells_mut(&mut self) -> std::slice::IterMut<'_, RenderTableCell> {
self.cells.iter_mut()
}
fn cells_drain(&mut self) -> impl Iterator<Item = RenderTableCell> + use<> {
std::mem::take(&mut self.cells).into_iter()
}
fn num_cells(&self) -> usize {
self.cells.iter().map(|cell| cell.colspan.max(1)).sum()
}
fn into_cells(self, vertical: bool) -> Vec<RenderNode> {
let mut result = Vec::new();
let mut colno = 0;
let col_sizes = self.col_sizes.unwrap();
let mut x_pos = 0;
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 {
let this_col_width = col_width + cell.colspan - 1;
cell.col_width = Some(this_col_width);
cell.x_pos = Some(x_pos);
x_pos += this_col_width + 1;
let style = cell.style.clone();
result.push(RenderNode::new_styled(
RenderNodeInfo::TableCell(cell),
style,
));
}
colno += colspan;
}
result
}
}
#[derive(Clone, Debug)]
struct RenderTable {
rows: Vec<RenderTableRow>,
num_columns: usize,
size_estimate: Cell<Option<SizeEstimate>>,
}
impl RenderTable {
fn new(mut rows: Vec<RenderTableRow>) -> RenderTable {
let mut col_positions = BTreeSet::new();
let mut overhang_cells: Vec<(usize, usize, usize)> = Vec::new();
let mut next_overhang_cells = Vec::new();
col_positions.insert(0);
for row in &mut rows {
let mut col = 0;
let mut new_cells = Vec::new();
for cell in row.cells_drain() {
while let Some(hanging) = overhang_cells.last() {
if hanging.1 <= col {
new_cells.push(RenderTableCell::dummy(hanging.2));
col += hanging.2;
col_positions.insert(col);
let mut used = overhang_cells.pop().unwrap();
if used.0 > 1 {
used.0 -= 1;
next_overhang_cells.push(used);
}
} else {
break;
}
}
if cell.rowspan > 1 {
next_overhang_cells.push((cell.rowspan - 1, col, cell.colspan));
}
col += cell.colspan;
col_positions.insert(col);
new_cells.push(cell);
}
while let Some(mut hanging) = overhang_cells.pop() {
new_cells.push(RenderTableCell::dummy(hanging.2));
col += hanging.2;
col_positions.insert(col);
if hanging.0 > 1 {
hanging.0 -= 1;
next_overhang_cells.push(hanging);
}
}
row.cells = new_cells;
overhang_cells = std::mem::take(&mut next_overhang_cells);
overhang_cells.reverse();
}
let colmap: HashMap<_, _> = col_positions
.into_iter()
.enumerate()
.map(|(i, pos)| (pos, i))
.collect();
for row in &mut rows {
let mut pos = 0;
let mut mapped_pos = 0;
for cell in row.cells_mut() {
let nextpos = pos + cell.colspan.max(1);
let next_mapped_pos = *colmap.get(&nextpos).unwrap();
cell.colspan = next_mapped_pos - mapped_pos;
pos = nextpos;
mapped_pos = next_mapped_pos;
}
}
let num_columns = rows.iter().map(|r| r.num_cells()).max().unwrap_or(0);
RenderTable {
rows,
num_columns,
size_estimate: Cell::new(None),
}
}
fn rows(&self) -> std::slice::Iter<'_, RenderTableRow> {
self.rows.iter()
}
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());
let style = tr.style.clone();
RenderNode::new_styled(RenderNodeInfo::TableRow(tr, vert), style)
})
.collect()
}
fn calc_size_estimate(&self, _context: &HtmlContext) -> SizeEstimate {
if self.num_columns == 0 {
let result = SizeEstimate {
size: 0,
min_width: 0,
prefix_size: 0,
};
self.size_estimate.set(Some(result));
return result;
}
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,
cellsize.min_width / cell.colspan,
);
}
colno += cell.colspan;
}
}
let size = sizes.iter().map(|s| s.size).sum::<usize>() + self.num_columns.saturating_sub(1);
let min_width = sizes.iter().map(|s| s.min_width).sum::<usize>() + self.num_columns - 1;
let result = SizeEstimate {
size,
min_width,
prefix_size: 0,
};
self.size_estimate.set(Some(result));
result
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
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),
Svg(String),
Block(Vec<RenderNode>),
Header(usize, Vec<RenderNode>),
Div(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),
ListItem(Vec<RenderNode>),
Sup(Vec<RenderNode>),
}
#[derive(Clone, Debug)]
struct RenderNode {
size_estimate: Cell<Option<SizeEstimate>>,
info: RenderNodeInfo,
style: ComputedStyle,
}
impl RenderNode {
fn new(info: RenderNodeInfo) -> RenderNode {
RenderNode {
size_estimate: Cell::new(None),
info,
style: Default::default(),
}
}
fn new_styled(info: RenderNodeInfo, style: ComputedStyle) -> RenderNode {
RenderNode {
size_estimate: Cell::new(None),
info,
style,
}
}
fn get_size_estimate(&self) -> SizeEstimate {
self.size_estimate.get().unwrap()
}
fn calc_size_estimate<D: TextDecorator>(
&self,
context: &HtmlContext,
decorator: &D,
) -> SizeEstimate {
if let Some(s) = self.size_estimate.get() {
return s;
};
use RenderNodeInfo::*;
let recurse = |node: &RenderNode| node.calc_size_estimate(context, decorator);
let estimate = match self.info {
Text(ref t) | Img(_, ref t) | Svg(ref t) => {
use unicode_width::UnicodeWidthChar;
let mut len = 0;
let mut in_whitespace = false;
for c in t.trim_collapsible_ws().chars() {
let is_collapsible_ws = !c.always_takes_space();
if !is_collapsible_ws {
len += UnicodeWidthChar::width(c).unwrap_or(0);
if in_whitespace {
len += 1;
}
}
in_whitespace = is_collapsible_ws;
}
if let Some(true) = t.chars().next().map(|c| !c.always_takes_space()) {
if len > 0 {
len += 1;
}
}
if let Img(_, _) = self.info {
len += 2;
}
SizeEstimate {
size: len,
min_width: len.min(context.min_wrap_width),
prefix_size: 0,
}
}
Container(ref v) | Em(ref v) | Strong(ref v) | Strikeout(ref v) | Code(ref v)
| Block(ref v) | Div(ref v) | Dl(ref v) | Dt(ref v) | ListItem(ref v) | Sup(ref v) => v
.iter()
.map(recurse)
.fold(Default::default(), SizeEstimate::add),
Link(ref _target, ref v) => v
.iter()
.map(recurse)
.fold(Default::default(), SizeEstimate::add)
.add(SizeEstimate {
size: 5,
min_width: 5,
prefix_size: 0,
}),
Dd(ref v) | BlockQuote(ref v) | Ul(ref v) => {
let prefix = match self.info {
Dd(_) => " ".into(),
BlockQuote(_) => decorator.quote_prefix(),
Ul(_) => decorator.unordered_item_prefix(),
_ => unreachable!(),
};
let prefix_width = UnicodeWidthStr::width(prefix.as_str());
let mut size = v
.iter()
.map(recurse)
.fold(Default::default(), SizeEstimate::add)
.add_hor(SizeEstimate {
size: prefix_width,
min_width: prefix_width,
prefix_size: 0,
});
size.prefix_size = prefix_width;
size
}
Ol(i, ref v) => {
let prefix_size = calc_ol_prefix_size(i, v.len(), decorator);
let mut result = v
.iter()
.map(recurse)
.fold(Default::default(), SizeEstimate::add)
.add_hor(SizeEstimate {
size: prefix_size,
min_width: prefix_size,
prefix_size: 0,
});
result.prefix_size = prefix_size;
result
}
Header(level, ref v) => {
let prefix_size = decorator.header_prefix(level).len();
let mut size = v
.iter()
.map(recurse)
.fold(Default::default(), SizeEstimate::add)
.add_hor(SizeEstimate {
size: prefix_size,
min_width: prefix_size,
prefix_size: 0,
});
size.prefix_size = prefix_size;
size
}
Break => SizeEstimate {
size: 1,
min_width: 1,
prefix_size: 0,
},
Table(ref t) => t.calc_size_estimate(context),
TableRow(..) | TableBody(_) | TableCell(_) => unimplemented!(),
FragStart(_) => Default::default(),
};
self.size_estimate.set(Some(estimate));
estimate
}
fn is_shallow_empty(&self) -> bool {
use RenderNodeInfo::*;
match self.info {
Text(ref t) | Img(_, ref t) | Svg(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)
| ListItem(ref v)
| Div(ref v)
| BlockQuote(ref v)
| Dl(ref v)
| Dt(ref v)
| Dd(ref v)
| Ul(ref v)
| Ol(_, ref v)
| Sup(ref v) => v.is_empty(),
Header(_level, ref v) => v.is_empty(),
Break => true,
Table(ref _t) => false,
TableRow(..) | TableBody(_) | TableCell(_) => false,
FragStart(_) => true,
}
}
fn write_container(
&self,
name: &str,
items: &[RenderNode],
f: &mut std::fmt::Formatter,
indent: usize,
) -> std::prelude::v1::Result<(), std::fmt::Error> {
writeln!(f, "{:indent$}{name}:", "")?;
for item in items {
item.write_self(f, indent + 1)?;
}
Ok(())
}
fn write_style(
f: &mut std::fmt::Formatter,
indent: usize,
style: &ComputedStyle,
) -> std::result::Result<(), std::fmt::Error> {
use std::fmt::Write;
let mut stylestr = String::new();
#[cfg(feature = "css")]
{
if let Some(col) = style.colour.val() {
write!(&mut stylestr, " colour={:?}", col)?;
}
if let Some(col) = style.bg_colour.val() {
write!(&mut stylestr, " bg_colour={:?}", col)?;
}
if let Some(val) = style.display.val() {
write!(&mut stylestr, " disp={:?}", val)?;
}
}
if let Some(ws) = style.white_space.val() {
write!(&mut stylestr, " white_space={:?}", ws)?;
}
if style.internal_pre {
write!(&mut stylestr, " internal_pre")?;
}
if !stylestr.is_empty() {
writeln!(f, "{:indent$}[Style:{stylestr}", "")?;
}
Ok(())
}
fn write_self(
&self,
f: &mut std::fmt::Formatter,
indent: usize,
) -> std::prelude::v1::Result<(), std::fmt::Error> {
Self::write_style(f, indent, &self.style)?;
match &self.info {
RenderNodeInfo::Text(s) => writeln!(f, "{:indent$}{s:?}", "")?,
RenderNodeInfo::Container(v) => {
self.write_container("Container", v, f, indent)?;
}
RenderNodeInfo::Link(targ, v) => {
self.write_container(&format!("Link({})", targ), v, f, indent)?;
}
RenderNodeInfo::Em(v) => {
self.write_container("Em", v, f, indent)?;
}
RenderNodeInfo::Strong(v) => {
self.write_container("Strong", v, f, indent)?;
}
RenderNodeInfo::Strikeout(v) => {
self.write_container("Strikeout", v, f, indent)?;
}
RenderNodeInfo::Code(v) => {
self.write_container("Code", v, f, indent)?;
}
RenderNodeInfo::Img(src, title) => {
writeln!(f, "{:indent$}Img src={:?} title={:?}:", "", src, title)?;
}
RenderNodeInfo::Svg(title) => {
writeln!(f, "{:indent$}Svg title={:?}:", "", title)?;
}
RenderNodeInfo::Block(v) => {
self.write_container("Block", v, f, indent)?;
}
RenderNodeInfo::Header(depth, v) => {
self.write_container(&format!("Header({})", depth), v, f, indent)?;
}
RenderNodeInfo::Div(v) => {
self.write_container("Div", v, f, indent)?;
}
RenderNodeInfo::BlockQuote(v) => {
self.write_container("BlockQuote", v, f, indent)?;
}
RenderNodeInfo::Ul(v) => {
self.write_container("Ul", v, f, indent)?;
}
RenderNodeInfo::Ol(start, v) => {
self.write_container(&format!("Ol({})", start), v, f, indent)?;
}
RenderNodeInfo::Dl(v) => {
self.write_container("Dl", v, f, indent)?;
}
RenderNodeInfo::Dt(v) => {
self.write_container("Dt", v, f, indent)?;
}
RenderNodeInfo::Dd(v) => {
self.write_container("Dd", v, f, indent)?;
}
RenderNodeInfo::Break => {
writeln!(f, "{:indent$}Break", "", indent = indent)?;
}
RenderNodeInfo::Table(rows) => {
writeln!(f, "{:indent$}Table ({} cols):", "", rows.num_columns)?;
for rtr in &rows.rows {
Self::write_style(f, indent + 1, &rtr.style)?;
writeln!(
f,
"{:width$}Row ({} cells):",
"",
rtr.cells.len(),
width = indent + 1
)?;
for cell in &rtr.cells {
Self::write_style(f, indent + 2, &cell.style)?;
writeln!(
f,
"{:width$}Cell colspan={} width={:?}:",
"",
cell.colspan,
cell.col_width,
width = indent + 2
)?;
for node in &cell.content {
node.write_self(f, indent + 3)?;
}
}
}
}
RenderNodeInfo::TableBody(_) => todo!(),
RenderNodeInfo::TableRow(_, _) => todo!(),
RenderNodeInfo::TableCell(_) => todo!(),
RenderNodeInfo::FragStart(frag) => {
writeln!(f, "{:indent$}FragStart({}):", "", frag)?;
}
RenderNodeInfo::ListItem(v) => {
self.write_container("ListItem", v, f, indent)?;
}
RenderNodeInfo::Sup(v) => {
self.write_container("Sup", v, f, indent)?;
}
}
Ok(())
}
}
fn precalc_size_estimate<'a, D: TextDecorator>(
node: &'a RenderNode,
context: &mut HtmlContext,
decorator: &'a D,
) -> TreeMapResult<'a, HtmlContext, &'a RenderNode, ()> {
use RenderNodeInfo::*;
if node.size_estimate.get().is_some() {
return TreeMapResult::Nothing;
}
match node.info {
Text(_) | Img(_, _) | Svg(_) | Break | FragStart(_) => {
let _ = node.calc_size_estimate(context, decorator);
TreeMapResult::Nothing
}
Container(ref v)
| Link(_, ref v)
| Em(ref v)
| Strong(ref v)
| Strikeout(ref v)
| Code(ref v)
| Block(ref v)
| ListItem(ref v)
| Div(ref v)
| BlockQuote(ref v)
| Ul(ref v)
| Ol(_, ref v)
| Dl(ref v)
| Dt(ref v)
| Dd(ref v)
| Sup(ref v)
| Header(_, ref v) => TreeMapResult::PendingChildren {
children: v.iter().collect(),
cons: Box::new(move |context, _cs| {
node.calc_size_estimate(context, decorator);
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 |context, _cs| {
node.calc_size_estimate(context, decorator);
Ok(None)
}),
prefn: None,
postfn: None,
}
}
TableRow(..) | TableBody(_) | TableCell(_) => unimplemented!(),
}
}
fn table_to_render_tree<'a, T: Write>(
input: RenderInput,
computed: ComputedStyle,
_err_out: &mut T,
) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode> {
pending(input, move |_, 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);
}
}
if rows.is_empty() {
None
} else {
Some(RenderNode::new_styled(
RenderNodeInfo::Table(RenderTable::new(rows)),
computed,
))
}
})
}
fn tbody_to_render_tree<'a, T: Write>(
input: RenderInput,
computed: ComputedStyle,
_err_out: &mut T,
) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode> {
pending_noempty(input, move |_, 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;
}
}
}
}
Some(RenderNode::new_styled(
RenderNodeInfo::TableBody(rows),
computed,
))
})
}
fn tr_to_render_tree<'a, T: Write>(
input: RenderInput,
computed: ComputedStyle,
_err_out: &mut T,
) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode> {
pending(input, move |_, 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();
let style = computed.clone();
Some(RenderNode::new_styled(
RenderNodeInfo::TableRow(
RenderTableRow {
cells,
col_sizes: None,
style,
},
false,
),
computed,
))
})
}
fn td_to_render_tree<'a, T: Write>(
input: RenderInput,
computed: ComputedStyle,
_err_out: &mut T,
) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode> {
let mut colspan = 1;
let mut rowspan = 1;
if let Element { ref attrs, .. } = input.handle.data {
for attr in attrs.borrow().iter() {
if &attr.name.local == "colspan" {
let v: &str = &attr.value;
colspan = v.parse().unwrap_or(1);
}
if &attr.name.local == "rowspan" {
let v: &str = &attr.value;
rowspan = v.parse().unwrap_or(1);
}
}
}
pending(input, move |_, children| {
let style = computed.clone();
Some(RenderNode::new_styled(
RenderNodeInfo::TableCell(RenderTableCell {
colspan,
rowspan,
content: children,
size_estimate: Cell::new(None),
col_width: None,
x_pos: None,
style,
is_dummy: false,
}),
computed,
))
})
}
type ResultReducer<'a, C, R> = dyn FnOnce(&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: FnMut(&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 last = PendingNode {
construct: Box::new(|_, mut cs| Ok(cs.pop())),
prefn: None,
postfn: None,
children: Vec::new(),
to_process: vec![top].into_iter(),
};
let mut pending_stack = Vec::new();
loop {
while let Some(h) = last.to_process.next() {
if let Some(f) = &last.prefn {
f(context, &h)?;
}
match process_node(context, h)? {
TreeMapResult::Finished(result) => {
if let Some(f) = &last.postfn {
f(context, &result)?;
}
last.children.push(result);
}
TreeMapResult::PendingChildren {
children,
cons,
prefn,
postfn,
} => {
pending_stack.push(last);
last = PendingNode {
construct: cons,
prefn,
postfn,
children: Vec::new(),
to_process: children.into_iter(),
};
}
TreeMapResult::Nothing => {}
};
}
if let Some(mut parent) = pending_stack.pop() {
if let Some(node) = (last.construct)(context, last.children)? {
if let Some(f) = &parent.postfn {
f(context, &node)?;
}
parent.children.push(node);
}
last = parent;
continue;
}
break Ok((last.construct)(context, last.children)?);
}
}
#[cfg(feature = "css_ext")]
#[derive(Clone, Default)]
struct HighlighterMap {
map: HashMap<String, Rc<SyntaxHighlighter>>,
}
#[cfg(feature = "css_ext")]
impl HighlighterMap {
pub fn get(&self, name: &str) -> Option<Rc<SyntaxHighlighter>> {
self.map.get(name).cloned()
}
fn insert(&mut self, name: impl Into<String>, f: Rc<SyntaxHighlighter>) {
self.map.insert(name.into(), f);
}
}
#[cfg(feature = "css_ext")]
impl std::fmt::Debug for HighlighterMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HighlighterMap")
.field("map", &self.map.keys().collect::<Vec<_>>())
.finish()
}
}
#[cfg(feature = "css_ext")]
impl PartialEq for HighlighterMap {
fn eq(&self, _other: &Self) -> bool {
todo!()
}
}
#[cfg(feature = "css_ext")]
impl Eq for HighlighterMap {}
#[derive(Debug, PartialEq, Eq)]
struct HtmlContext {
style_data: css::StyleData,
#[cfg(feature = "css")]
use_doc_css: bool,
max_wrap_width: Option<usize>,
pad_block_width: bool,
allow_width_overflow: bool,
min_wrap_width: usize,
raw: bool,
draw_borders: bool,
wrap_links: bool,
include_link_footnotes: bool,
use_unicode_strikeout: bool,
image_mode: config::ImageRenderMode,
#[cfg(feature = "xml")]
xml_mode: config::XmlMode,
#[cfg(feature = "css_ext")]
syntax_highlighters: HighlighterMap,
}
struct RenderInput {
handle: Handle,
parent_style: Rc<ComputedStyle>,
#[cfg(feature = "css_ext")]
extra_styles: RefCell<Vec<(Range<usize>, TextStyle)>>,
node_lengths: Rc<RefCell<HashMap<*const Node, usize>>>,
}
impl RenderInput {
fn new(handle: Handle, parent_style: Rc<ComputedStyle>) -> Self {
RenderInput {
handle,
parent_style,
#[cfg(feature = "css_ext")]
extra_styles: Default::default(),
node_lengths: Default::default(),
}
}
#[cfg(feature = "css_ext")]
fn set_syntax_info(&self, full_text: &str, highlighted: Vec<(TextStyle, &str)>) {
let mut node_styles = Vec::new();
for (style, s) in highlighted {
fn get_offset(full: &str, sub: &str) -> Option<Range<usize>> {
let full_start = full.as_ptr() as usize;
let full_end = full_start + full.len();
let sub_start = sub.as_ptr() as usize;
let sub_end = sub_start + sub.len();
if sub_start >= full_start && sub_end <= full_end {
Some((sub_start - full_start)..(sub_end - full_start))
} else {
None
}
}
if let Some(offset_range) = get_offset(full_text, s) {
node_styles.push((offset_range, style));
} }
node_styles.sort_by_key(|r| (r.0.start, r.0.end));
*self.extra_styles.borrow_mut() = node_styles;
}
#[allow(clippy::mut_range_bound)]
fn children(&self) -> Vec<RenderInput> {
#[cfg(feature = "css_ext")]
if !self.extra_styles.borrow().is_empty() {
let mut offset = 0;
let mut result = Vec::new();
let mut start_style_index = 0;
let node_lengths = self.node_lengths.borrow();
let extra_styles = self.extra_styles.borrow();
for child in &*self.handle.children.borrow() {
let end_offset = offset + node_lengths.get(&Rc::as_ptr(child)).unwrap();
let mut child_extra_styles = Vec::new();
for es_idx in start_style_index..extra_styles.len() {
let mut style_range = extra_styles[es_idx].0.clone();
if style_range.start >= end_offset {
break;
}
if style_range.end <= offset {
start_style_index = es_idx;
} else {
style_range.start = style_range.start.max(offset) - offset;
style_range.end = style_range.end.min(end_offset) - offset;
child_extra_styles.push((style_range, extra_styles[es_idx].1.clone()));
}
}
result.push(RenderInput {
handle: Rc::clone(child),
parent_style: Rc::clone(&self.parent_style),
extra_styles: RefCell::new(child_extra_styles),
node_lengths: self.node_lengths.clone(),
});
offset = end_offset;
}
return result;
}
self.handle
.children
.borrow()
.iter()
.map(|child| RenderInput {
handle: child.clone(),
parent_style: Rc::clone(&self.parent_style),
#[cfg(feature = "css_ext")]
extra_styles: Default::default(),
node_lengths: self.node_lengths.clone(),
})
.collect()
}
#[cfg(feature = "css_ext")]
fn do_extract_text(
out: &mut String,
handle: &Handle,
length_map: &mut HashMap<*const Node, usize>,
) {
match handle.data {
markup5ever_rcdom::NodeData::Text { contents: ref tstr } => {
let s: &str = &tstr.borrow();
out.push_str(s);
length_map.entry(Rc::as_ptr(handle)).or_insert(s.len());
}
_ => {
for child in handle.children.borrow().iter() {
let len_before = out.len();
RenderInput::do_extract_text(out, child, length_map);
let len_after = out.len();
length_map
.entry(Rc::as_ptr(child))
.or_insert(len_after - len_before);
}
}
}
}
#[cfg(feature = "css_ext")]
fn extract_raw_text(&self) -> String {
let mut result = String::new();
RenderInput::do_extract_text(
&mut result,
&self.handle,
&mut self.node_lengths.borrow_mut(),
);
result
}
}
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_extract::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 parent_style = Default::default();
let result = tree_map_reduce(
context,
RenderInput::new(handle, parent_style),
|context, input| process_dom_node(input, err_out, context),
);
html_trace!("### dom_to_render_tree: out= {:#?}", result);
result
}
#[cfg(feature = "css")]
pub fn dom_to_parsed_style(dom: &RcDom) -> Result<String> {
let handle = dom.document.clone();
let doc_style_data = css::dom_extract::dom_to_stylesheet(handle, &mut std::io::sink())?;
Ok(doc_style_data.to_string())
}
fn pending<F>(
input: RenderInput,
f: F,
) -> TreeMapResult<'static, HtmlContext, RenderInput, RenderNode>
where
F: FnOnce(&mut HtmlContext, Vec<RenderNode>) -> Option<RenderNode> + 'static,
{
TreeMapResult::PendingChildren {
children: input.children(),
cons: Box::new(move |ctx, children| Ok(f(ctx, children))),
prefn: None,
postfn: None,
}
}
fn pending_noempty<F>(
input: RenderInput,
f: F,
) -> TreeMapResult<'static, HtmlContext, RenderInput, RenderNode>
where
F: FnOnce(&mut HtmlContext, Vec<RenderNode>) -> Option<RenderNode> + 'static,
{
let handle = &input.handle;
let style = &input.parent_style;
TreeMapResult::PendingChildren {
children: handle
.children
.borrow()
.iter()
.map(|child| RenderInput::new(child.clone(), Rc::clone(style)))
.collect(),
cons: Box::new(move |ctx, children| {
if children.is_empty() {
Ok(None)
} else {
Ok(f(ctx, children))
}
}),
prefn: None,
postfn: None,
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum ChildPosition {
Start,
End,
}
fn insert_child(
new_child: RenderNode,
mut orig: RenderNode,
position: ChildPosition,
) -> RenderNode {
use RenderNodeInfo::*;
html_trace!("insert_child({:?}, {:?}, {:?})", new_child, orig, position);
match orig.info {
Block(ref mut children)
| ListItem(ref mut children)
| Dd(ref mut children)
| Dt(ref mut children)
| Dl(ref mut children)
| Div(ref mut children)
| BlockQuote(ref mut children)
| Container(ref mut children)
| TableCell(RenderTableCell {
content: ref mut children,
..
}) => {
match position {
ChildPosition::Start => children.insert(0, new_child),
ChildPosition::End => children.push(new_child),
}
}
TableRow(ref mut rrow, _) => {
if let Some(cell) = rrow.cells.first_mut() {
match position {
ChildPosition::Start => cell.content.insert(0, new_child),
ChildPosition::End => cell.content.push(new_child),
}
}
}
TableBody(ref mut rows) | Table(RenderTable { ref mut rows, .. }) => {
if let Some(rrow) = rows.first_mut() {
if let Some(cell) = rrow.cells.first_mut() {
match position {
ChildPosition::Start => cell.content.insert(0, new_child),
ChildPosition::End => cell.content.push(new_child),
}
}
}
}
_ => {
let result = match position {
ChildPosition::Start => RenderNode::new(Container(vec![new_child, orig])),
ChildPosition::End => RenderNode::new(Container(vec![orig, new_child])),
};
html_trace!("insert_child() -> {:?}", result);
return result;
}
}
html_trace!("insert_child() -> {:?}", &orig);
orig
}
fn process_dom_node<T: Write>(
input: RenderInput,
err_out: &mut T,
#[allow(unused)] context: &mut HtmlContext,
) -> Result<TreeMapResult<'static, HtmlContext, RenderInput, RenderNode>> {
use RenderNodeInfo::*;
use TreeMapResult::*;
Ok(match input.handle.clone().data {
Document => pending(input, |_context, cs| Some(RenderNode::new(Container(cs)))),
Comment { .. } => Nothing,
Element {
ref name,
ref attrs,
..
} => {
let mut frag_from_name_attr = false;
let RenderInput {
ref handle,
ref parent_style,
..
} = input;
#[cfg(feature = "css")]
let use_doc_css = context.use_doc_css;
#[cfg(not(feature = "css"))]
let use_doc_css = false;
let computed = {
let computed = context
.style_data
.computed_style(parent_style, handle, use_doc_css);
#[cfg(feature = "css")]
match computed.display.val() {
Some(css::Display::None) => return Ok(Nothing),
#[cfg(feature = "css_ext")]
Some(css::Display::ExtRawDom) => {
use html5ever::interface::{NodeOrText, TreeSink};
use html5ever::{LocalName, QualName};
let mut html_bytes: Vec<u8> = Default::default();
handle.serialize(&mut html_bytes)?;
let dom = RcDom::default();
let html_string = String::from_utf8_lossy(&html_bytes).into_owned();
let pre_node = dom.create_element(
QualName::new(None, ns!(html), LocalName::from("pre")),
vec![],
Default::default(),
);
dom.append(&pre_node, NodeOrText::AppendText(html_string.into()));
let mut my_computed = computed;
my_computed.display = Default::default();
my_computed.white_space.maybe_update(
false,
StyleOrigin::Agent,
Default::default(),
WhiteSpace::Pre,
);
my_computed.internal_pre = true;
let new_input = RenderInput {
handle: pre_node,
parent_style: Rc::new(my_computed.clone()),
extra_styles: Default::default(),
node_lengths: Default::default(),
};
if let Some(syntax_info) = my_computed.syntax.val() {
if let Some(highlighter) =
context.syntax_highlighters.get(&syntax_info.language)
{
let text = new_input.extract_raw_text();
let highlighted = highlighter(&text);
new_input.set_syntax_info(&text, highlighted);
}
}
return Ok(pending(new_input, move |_, cs| {
Some(RenderNode::new_styled(Container(cs), my_computed))
}));
}
_ => (),
}
#[cfg(feature = "css_ext")]
if let Some(syntax_info) = computed.syntax.val() {
if let Some(highlighter) =
context.syntax_highlighters.get(&syntax_info.language)
{
let extracted_text = input.extract_raw_text();
let highlighted = highlighter(&extracted_text);
input.set_syntax_info(&extracted_text, highlighted);
}
}
computed
};
let computed_before = computed.content_before.clone();
let computed_after = computed.content_after.clone();
let result = match name.expanded() {
expanded_name!(html "html") | expanded_name!(html "body") => {
pending(input, move |_, cs| {
Some(RenderNode::new_styled(Container(cs), computed))
})
}
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") => {
pending_noempty(input, move |_, cs| {
Some(RenderNode::new_styled(Container(cs), computed))
})
}
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: input.children(),
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_styled(Link(href, cs), computed)))
} else {
Ok(None)
}
})
} else {
Box::new(move |_, cs| {
Ok(Some(RenderNode::new_styled(Container(cs), computed)))
})
},
prefn: None,
postfn: None,
}
}
expanded_name!(html "em")
| expanded_name!(html "i")
| expanded_name!(html "ins") => pending(input, move |_, cs| {
Some(RenderNode::new_styled(Em(cs), computed))
}),
expanded_name!(html "strong") | expanded_name!(html "b") => {
pending(input, move |_, cs| {
Some(RenderNode::new_styled(Strong(cs), computed))
})
}
expanded_name!(html "s") | expanded_name!(html "del") => {
pending(input, move |_, cs| {
Some(RenderNode::new_styled(Strikeout(cs), computed))
})
}
expanded_name!(html "code") => pending(input, move |_, cs| {
Some(RenderNode::new_styled(Code(cs), computed))
}),
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(src) = src {
Finished(RenderNode::new_styled(
Img(src.into(), title.unwrap_or("").into()),
computed,
))
} else {
Nothing
}
}
expanded_name!(svg "svg") => {
let mut title = None;
for node in input.handle.children.borrow().iter() {
if let markup5ever_rcdom::NodeData::Element { ref name, .. } = node.data {
if matches!(name.expanded(), expanded_name!(svg "title")) {
let mut title_str = String::new();
for subnode in node.children.borrow().iter() {
if let markup5ever_rcdom::NodeData::Text { ref contents } =
subnode.data
{
title_str.push_str(&contents.borrow());
}
}
title = Some(title_str);
} else {
break;
}
}
}
Finished(RenderNode::new_styled(
Svg(title.unwrap_or_else(String::new)),
computed,
))
}
expanded_name!(html "h1")
| expanded_name!(html "h2")
| expanded_name!(html "h3")
| expanded_name!(html "h4")
| expanded_name!(html "h5")
| expanded_name!(html "h6") => {
let level: usize = name.local[1..].parse().unwrap();
pending(input, move |_, cs| {
Some(RenderNode::new_styled(Header(level, cs), computed))
})
}
expanded_name!(html "p") => pending_noempty(input, move |_, cs| {
Some(RenderNode::new_styled(Block(cs), computed))
}),
expanded_name!(html "li") => pending(input, move |_, cs| {
Some(RenderNode::new_styled(ListItem(cs), computed))
}),
expanded_name!(html "sup") => pending(input, move |_, cs| {
Some(RenderNode::new_styled(Sup(cs), computed))
}),
expanded_name!(html "div") => pending_noempty(input, move |_, cs| {
Some(RenderNode::new_styled(Div(cs), computed))
}),
expanded_name!(html "pre") => pending(input, move |_, cs| {
let mut computed = computed;
computed.white_space.maybe_update(
false,
StyleOrigin::Agent,
Default::default(),
WhiteSpace::Pre,
);
computed.internal_pre = true;
Some(RenderNode::new_styled(Block(cs), computed))
}),
expanded_name!(html "br") => Finished(RenderNode::new_styled(Break, computed)),
expanded_name!(html "wbr") => {
Finished(RenderNode::new_styled(Text("\u{200b}".into()), computed))
}
expanded_name!(html "table") => table_to_render_tree(input, computed, err_out),
expanded_name!(html "thead") | expanded_name!(html "tbody") => {
tbody_to_render_tree(input, computed, err_out)
}
expanded_name!(html "tr") => tr_to_render_tree(input, computed, err_out),
expanded_name!(html "th") | expanded_name!(html "td") => {
td_to_render_tree(input, computed, err_out)
}
expanded_name!(html "blockquote") => pending_noempty(input, move |_, cs| {
Some(RenderNode::new_styled(BlockQuote(cs), computed))
}),
expanded_name!(html "ul") => pending_noempty(input, move |_, cs| {
Some(RenderNode::new_styled(Ul(cs), computed))
}),
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;
}
}
pending_noempty(input, move |_, cs| {
let cs = cs
.into_iter()
.filter(|n| matches!(n.info, RenderNodeInfo::ListItem(..)))
.collect();
Some(RenderNode::new_styled(Ol(start, cs), computed))
})
}
expanded_name!(html "dl") => {
pending_noempty(input, move |_, cs| {
let cs = cs
.into_iter()
.filter(|n| {
matches!(n.info, RenderNodeInfo::Dt(..) | RenderNodeInfo::Dd(..))
})
.collect();
Some(RenderNode::new_styled(Dl(cs), computed))
})
}
expanded_name!(html "dt") => pending(input, move |_, cs| {
Some(RenderNode::new_styled(Dt(cs), computed))
}),
expanded_name!(html "dd") => pending(input, move |_, cs| {
Some(RenderNode::new_styled(Dd(cs), computed))
}),
_ => {
html_trace!("Unhandled element: {:?}\n", name.local);
pending_noempty(input, move |_, cs| {
Some(RenderNode::new_styled(Container(cs), computed))
})
}
};
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;
}
}
let result = if computed_before.is_some() || computed_after.is_some() {
let wrap_nodes = move |mut node: RenderNode| {
if let Some(ref content) = computed_before {
if let Some(pseudo_content) = content.content.val() {
node = insert_child(
RenderNode::new(Text(pseudo_content.text.clone())),
node,
ChildPosition::Start,
);
}
}
if let Some(ref content) = computed_after {
if let Some(pseudo_content) = content.content.val() {
node = insert_child(
RenderNode::new(Text(pseudo_content.text.clone())),
node,
ChildPosition::End,
);
}
}
node
};
match result {
Finished(node) => Finished(wrap_nodes(node)),
Nothing => Nothing,
PendingChildren {
children,
cons,
prefn,
postfn,
} => PendingChildren {
children,
prefn,
postfn,
cons: Box::new(move |ctx, ch| match cons(ctx, ch)? {
None => Ok(None),
Some(node) => Ok(Some(wrap_nodes(node))),
}),
},
}
} else {
result
};
let Some(fragname) = fragment else {
return Ok(result);
};
match result {
Finished(node) => Finished(insert_child(
RenderNode::new(FragStart(fragname)),
node,
ChildPosition::Start,
)),
Nothing => Finished(RenderNode::new(FragStart(fragname))),
PendingChildren {
children,
cons,
prefn,
postfn,
} => PendingChildren {
children,
prefn,
postfn,
cons: Box::new(move |ctx, ch| {
let fragnode = RenderNode::new(FragStart(fragname));
match cons(ctx, ch)? {
None => Ok(Some(fragnode)),
Some(node) => {
Ok(Some(insert_child(fragnode, node, ChildPosition::Start)))
}
}
}),
},
}
}
markup5ever_rcdom::NodeData::Text { contents: ref tstr } => {
#[cfg(feature = "css_ext")]
if !input.extra_styles.borrow().is_empty() {
let mut nodes = Vec::new();
let mut offset = 0;
for part in &*input.extra_styles.borrow() {
let (start, end) = (part.0.start, part.0.end);
if start > offset {
nodes.push(RenderNode::new(Text((tstr.borrow()[offset..start]).into())));
}
let mut cstyle = input.parent_style.inherit();
cstyle.colour.maybe_update(
cstyle.syntax.important,
cstyle.syntax.origin,
cstyle.syntax.specificity,
part.1.fg_colour,
);
if let Some(bgcol) = part.1.bg_colour {
cstyle.bg_colour.maybe_update(
cstyle.syntax.important,
cstyle.syntax.origin,
cstyle.syntax.specificity,
bgcol,
);
}
nodes.push(RenderNode::new_styled(
Text((tstr.borrow()[start..end]).into()),
cstyle,
));
offset = end;
}
if offset < tstr.borrow().len() {
nodes.push(RenderNode::new(Text((tstr.borrow()[offset..]).into())));
}
if nodes.len() == 1 {
return Ok(Finished(nodes.pop().unwrap()));
} else {
return Ok(Finished(RenderNode::new(RenderNodeInfo::Container(nodes))));
}
}
Finished(RenderNode::new(Text((&*tstr.borrow()).into())))
}
_ => {
writeln!(err_out, "Unhandled node type.").unwrap();
Nothing
}
})
}
fn render_tree_to_string<T: Write, D: TextDecorator>(
context: &mut HtmlContext,
renderer: SubRenderer<D>,
decorator: &D,
tree: RenderNode,
err_out: &mut T,
) -> Result<SubRenderer<D>> {
tree_map_reduce(context, &tree, |context, node| {
Ok(precalc_size_estimate(node, context, decorator))
})?;
let mut renderer = TextRenderer::new(renderer);
tree_map_reduce(&mut renderer, tree, |renderer, node| {
Ok(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<
D: TextDecorator,
F: FnOnce(
&mut TextRenderer<D>,
Vec<Option<SubRenderer<D>>>,
) -> Result<Option<Option<SubRenderer<D>>>>
+ 'static,
>(
children: Vec<RenderNode>,
f: F,
) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
TreeMapResult::PendingChildren {
children,
cons: Box::new(f),
prefn: None,
postfn: None,
}
}
#[derive(Default)]
struct PushedStyleInfo {
colour: bool,
bgcolour: bool,
white_space: bool,
preformat: bool,
}
impl PushedStyleInfo {
fn apply<D: TextDecorator>(render: &mut TextRenderer<D>, style: &ComputedStyle) -> Self {
#[allow(unused_mut)]
let mut result: PushedStyleInfo = Default::default();
#[cfg(feature = "css")]
if let Some(col) = style.colour.val() {
render.push_colour(*col);
result.colour = true;
}
#[cfg(feature = "css")]
if let Some(col) = style.bg_colour.val() {
render.push_bgcolour(*col);
result.bgcolour = true;
}
if let Some(ws) = style.white_space.val() {
if let WhiteSpace::Pre | WhiteSpace::PreWrap = ws {
render.push_ws(*ws);
result.white_space = true;
}
}
if style.internal_pre {
render.push_preformat();
result.preformat = true;
}
result
}
fn unwind<D: TextDecorator>(self, renderer: &mut TextRenderer<D>) {
if self.bgcolour {
renderer.pop_bgcolour();
}
if self.colour {
renderer.pop_colour();
}
if self.white_space {
renderer.pop_ws();
}
if self.preformat {
renderer.pop_preformat();
}
}
}
fn do_render_node<T: Write, D: TextDecorator>(
renderer: &mut TextRenderer<D>,
tree: RenderNode,
err_out: &mut T,
) -> render::Result<TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>>> {
html_trace!("do_render_node({:?}", tree);
use RenderNodeInfo::*;
use TreeMapResult::*;
let size_estimate = tree.size_estimate.get().unwrap_or_default();
let pushed_style = PushedStyleInfo::apply(renderer, &tree.style);
Ok(match tree.info {
Text(ref tstr) => {
renderer.add_inline_text(tstr)?;
pushed_style.unwind(renderer);
Finished(None)
}
Container(children) => pending2(children, |renderer, _| {
pushed_style.unwind(renderer);
Ok(Some(None))
}),
Link(href, children) => {
renderer.start_link(&href)?;
pending2(children, move |renderer: &mut TextRenderer<D>, _| {
renderer.end_link()?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Em(children) => {
renderer.start_emphasis()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_emphasis()?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Strong(children) => {
renderer.start_strong()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_strong()?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Strikeout(children) => {
renderer.start_strikeout()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_strikeout()?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Code(children) => {
renderer.start_code()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_code()?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Img(src, title) => {
renderer.add_image(&src, &title)?;
pushed_style.unwind(renderer);
Finished(None)
}
Svg(title) => {
renderer.add_image("", &title)?;
pushed_style.unwind(renderer);
Finished(None)
}
Block(children) | ListItem(children) => {
renderer.start_block()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_block();
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Header(level, children) => {
let prefix = renderer.header_prefix(level);
let prefix_size = size_estimate.prefix_size;
debug_assert!(prefix.len() == prefix_size);
let min_width = size_estimate.min_width;
let inner_width = min_width.saturating_sub(prefix_size);
let sub_builder =
renderer.new_sub_renderer(renderer.width_minus(prefix_size, inner_width)?)?;
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();
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Div(children) => {
renderer.new_line()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.new_line()?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
BlockQuote(children) => {
let prefix = renderer.quote_prefix();
debug_assert!(size_estimate.prefix_size == prefix.len());
let inner_width = size_estimate.min_width - prefix.len();
let sub_builder =
renderer.new_sub_renderer(renderer.width_minus(prefix.len(), inner_width)?)?;
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();
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Ul(items) => {
let prefix = renderer.unordered_item_prefix();
let prefix_len = prefix.len();
TreeMapResult::PendingChildren {
children: items,
cons: Box::new(|renderer, _| {
pushed_style.unwind(renderer);
Ok(Some(None))
}),
prefn: Some(Box::new(move |renderer: &mut TextRenderer<D>, _| {
let inner_width = size_estimate.min_width - prefix_len;
let sub_builder = renderer
.new_sub_renderer(renderer.width_minus(prefix_len, inner_width)?)?;
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) => {
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(|renderer, _| {
pushed_style.unwind(renderer);
Ok(Some(None))
}),
prefn: Some(Box::new(move |renderer: &mut TextRenderer<D>, _| {
let inner_min = size_estimate.min_width - size_estimate.prefix_size;
let sub_builder = renderer
.new_sub_renderer(renderer.width_minus(prefix_width, inner_min)?)?;
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(|renderer, _| {
pushed_style.unwind(renderer);
Ok(Some(None))
}),
prefn: None,
postfn: None,
}
}
Dt(children) => {
renderer.new_line()?;
renderer.start_emphasis()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_emphasis()?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Dd(children) => {
let inner_min = size_estimate.min_width - 2;
let sub_builder = renderer.new_sub_renderer(renderer.width_minus(2, inner_min)?)?;
renderer.push(sub_builder);
pending2(children, |renderer: &mut TextRenderer<D>, _| {
let sub_builder = renderer.pop();
renderer.append_subrender(sub_builder, repeat(" "))?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
Break => {
renderer.new_line_hard()?;
pushed_style.unwind(renderer);
Finished(None)
}
Table(tab) => render_table_tree(renderer, tab, err_out)?,
TableRow(row, false) => render_table_row(renderer, row, pushed_style, err_out),
TableRow(row, true) => render_table_row_vert(renderer, row, pushed_style, err_out),
TableBody(_) => unimplemented!("Unexpected TableBody while rendering"),
TableCell(cell) => render_table_cell(renderer, cell, pushed_style, err_out),
FragStart(fragname) => {
renderer.record_frag_start(&fragname);
pushed_style.unwind(renderer);
Finished(None)
}
Sup(children) => {
fn sup_digits(children: &[RenderNode]) -> Option<String> {
let [node] = children else {
return None;
};
if let Text(s) = &node.info {
if s.chars().all(|d| d.is_ascii_digit()) {
const SUPERSCRIPTS: [char; 10] =
['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
return Some(
s.bytes()
.map(|b| SUPERSCRIPTS[(b - b'0') as usize])
.collect(),
);
}
}
None
}
if let Some(digitstr) = sup_digits(&children) {
renderer.add_inline_text(&digitstr)?;
pushed_style.unwind(renderer);
Finished(None)
} else {
renderer.start_superscript()?;
pending2(children, |renderer: &mut TextRenderer<D>, _| {
renderer.end_superscript()?;
pushed_style.unwind(renderer);
Ok(Some(None))
})
}
}
})
}
fn render_table_tree<T: Write, D: TextDecorator>(
renderer: &mut TextRenderer<D>,
table: RenderTable,
_err_out: &mut T,
) -> render::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 = renderer.options.raw || (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().sum::<usize>() + num_cols - 1;
if cur_width <= width {
break;
}
let (i, _) = col_widths
.iter()
.enumerate()
.max_by_key(|&(colno, width)| {
(
width.saturating_sub(col_sizes[colno].min_width),
width,
usize::MAX - colno,
)
})
.unwrap();
col_widths[i] -= 1;
}
}
}
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.start_table()?;
if table_width != 0 && renderer.options.draw_borders {
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: None,
postfn: None,
})
}
fn render_table_row<T: Write, D: TextDecorator>(
_renderer: &mut TextRenderer<D>,
row: RenderTableRow,
pushed_style: PushedStyleInfo,
_err_out: &mut T,
) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
let rowspans: Vec<usize> = row.cells().map(|cell| cell.rowspan).collect();
let have_overhang = row.cells().any(|cell| cell.is_dummy);
TreeMapResult::PendingChildren {
children: row.into_cells(false),
cons: Box::new(move |builders, children| {
let children: Vec<_> = children.into_iter().map(Option::unwrap).collect();
if have_overhang || children.iter().any(|c| !c.empty()) {
builders.append_columns_with_borders(
children.into_iter().zip(rowspans.into_iter()),
true,
)?;
}
pushed_style.unwind(builders);
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,
pushed_style: PushedStyleInfo,
_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)?;
pushed_style.unwind(builders);
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,
pushed_style: PushedStyleInfo,
_err_out: &mut T,
) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
pending2(cell.content, |renderer: &mut TextRenderer<D>, _| {
pushed_style.unwind(renderer);
let sub_builder = renderer.pop();
Ok(Some(Some(sub_builder)))
})
}
pub mod config {
use std::io;
use super::Error;
use crate::css::types::Importance;
use crate::css::{Ruleset, Selector, SelectorComponent, Style, StyleData};
#[cfg(feature = "css_ext")]
use crate::{HighlighterMap, SyntaxHighlighter};
use crate::{
HtmlContext, MIN_WIDTH, RenderTree, Result,
css::{PseudoContent, PseudoElement, StyleDecl},
render::text_renderer::{
PlainDecorator, RichAnnotation, RichDecorator, TaggedLine, TextDecorator,
},
};
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImageRenderMode {
#[default]
IgnoreEmpty,
ShowAlways,
Replace(&'static str),
Filename,
}
#[cfg(feature = "xml")]
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum XmlMode {
#[default]
Auto,
Html,
Xhtml,
}
pub struct Config<D: TextDecorator> {
decorator: D,
max_wrap_width: Option<usize>,
style: StyleData,
#[cfg(feature = "css")]
use_doc_css: bool,
pad_block_width: bool,
allow_width_overflow: bool,
min_wrap_width: usize,
raw: bool,
draw_borders: bool,
wrap_links: bool,
include_link_footnotes: bool,
use_unicode_strikeout: bool,
image_mode: ImageRenderMode,
#[cfg(feature = "xml")]
xml_mode: XmlMode,
#[cfg(feature = "css_ext")]
syntax_highlighters: HighlighterMap,
}
impl<D: TextDecorator> Config<D> {
pub(crate) fn make_context(&self) -> HtmlContext {
HtmlContext {
style_data: self.style.clone(),
#[cfg(feature = "css")]
use_doc_css: self.use_doc_css,
max_wrap_width: self.max_wrap_width,
pad_block_width: self.pad_block_width,
allow_width_overflow: self.allow_width_overflow,
min_wrap_width: self.min_wrap_width,
raw: self.raw,
draw_borders: self.draw_borders,
wrap_links: self.wrap_links,
include_link_footnotes: self.include_link_footnotes,
use_unicode_strikeout: self.use_unicode_strikeout,
image_mode: self.image_mode,
#[cfg(feature = "xml")]
xml_mode: self.xml_mode,
#[cfg(feature = "css_ext")]
syntax_highlighters: self.syntax_highlighters.clone(),
}
}
pub(crate) fn do_parse<R>(&self, context: &mut HtmlContext, input: R) -> Result<RenderTree>
where
R: io::Read,
{
#[cfg(feature = "xml")]
let dom = {
match context.xml_mode {
XmlMode::Html => self.parse_html(input)?,
XmlMode::Xhtml => self.parse_xml(input)?,
XmlMode::Auto => {
const XML_CHECK: &[u8] = b"<?xml";
let mut input = input;
let mut firstbuf = [0u8; XML_CHECK.len()];
let bytes_read = input.read(&mut firstbuf)?;
let first_slice = &firstbuf[..bytes_read];
if bytes_read == XML_CHECK.len() && &firstbuf == XML_CHECK {
self.parse_xml(std::io::Read::chain(first_slice, input))?
} else {
self.parse_html(std::io::Read::chain(first_slice, input))?
}
}
}
};
#[cfg(not(feature = "xml"))]
let dom = self.parse_html(input)?;
let render_tree = super::dom_to_render_tree_with_context(
dom.document.clone(),
&mut io::sink(),
context,
)?
.ok_or(Error::Fail)?;
Ok(RenderTree(render_tree))
}
pub fn parse_html<R: io::Read>(&self, mut input: R) -> Result<super::RcDom> {
use html5ever::tendril::TendrilSink;
let opts = super::ParseOpts {
tree_builder: super::TreeBuilderOpts {
scripting_enabled: false,
..Default::default()
},
..Default::default()
};
Ok(super::parse_document(super::RcDom::default(), opts)
.from_utf8()
.read_from(&mut input)?)
}
#[cfg(feature = "xml")]
pub fn parse_xml<R: io::Read>(&self, mut input: R) -> Result<super::RcDom> {
use ::xml5ever::{driver::parse_document, tendril::TendrilSink};
let opts = Default::default();
Ok(parse_document(super::RcDom::default(), opts)
.from_utf8()
.read_from(&mut input)?)
}
pub fn dom_to_render_tree(&self, dom: &super::RcDom) -> Result<RenderTree> {
Ok(RenderTree(
super::dom_to_render_tree_with_context(
dom.document.clone(),
&mut io::sink(),
&mut self.make_context(),
)?
.ok_or(Error::Fail)?,
))
}
pub fn render_to_string(&self, render_tree: RenderTree, width: usize) -> Result<String> {
let s = render_tree
.render_with_context(
&mut self.make_context(),
width,
self.decorator.make_subblock_decorator(),
)?
.into_string()?;
Ok(s)
}
pub fn render_to_lines(
&self,
render_tree: RenderTree,
width: usize,
) -> Result<Vec<TaggedLine<Vec<D::Annotation>>>> {
render_tree
.render_with_context(
&mut self.make_context(),
width,
self.decorator.make_subblock_decorator(),
)?
.into_lines()
}
pub fn string_from_read<R: std::io::Read>(self, input: R, width: usize) -> Result<String> {
let mut context = self.make_context();
let s = self
.do_parse(&mut context, input)?
.render_with_context(&mut context, width, self.decorator)?
.into_string()?;
Ok(s)
}
pub fn lines_from_read<R: std::io::Read>(
self,
input: R,
width: usize,
) -> Result<Vec<TaggedLine<Vec<D::Annotation>>>> {
let mut context = self.make_context();
self.do_parse(&mut context, input)?
.render_with_context(&mut context, width, self.decorator)?
.into_lines()
}
#[cfg(feature = "css")]
pub fn add_css(mut self, css: &str) -> Result<Self> {
self.style.add_user_css(css)?;
Ok(self)
}
#[cfg(feature = "css")]
pub fn add_agent_css(mut self, css: &str) -> Result<Self> {
self.style.add_agent_css(css)?;
Ok(self)
}
#[cfg(feature = "css")]
pub fn use_doc_css(mut self) -> Self {
self.use_doc_css = true;
self
}
pub fn pad_block_width(mut self) -> Self {
self.pad_block_width = true;
self
}
pub fn max_wrap_width(mut self, wrap_width: usize) -> Self {
self.max_wrap_width = Some(wrap_width);
self
}
pub fn allow_width_overflow(mut self) -> Self {
self.allow_width_overflow = true;
self
}
pub fn min_wrap_width(mut self, min_wrap_width: usize) -> Self {
self.min_wrap_width = min_wrap_width;
self
}
pub fn raw_mode(mut self, raw: bool) -> Self {
self.raw = raw;
self.draw_borders = false;
self
}
pub fn no_table_borders(mut self) -> Self {
self.draw_borders = false;
self
}
pub fn no_link_wrapping(mut self) -> Self {
self.wrap_links = false;
self
}
pub fn unicode_strikeout(mut self, use_unicode: bool) -> Self {
self.use_unicode_strikeout = use_unicode;
self
}
fn make_surround_rule(element: &str, after: bool, content: &str) -> Ruleset {
Ruleset {
selector: Selector {
components: vec![SelectorComponent::Element(element.into())],
pseudo_element: Some(if after {
PseudoElement::After
} else {
PseudoElement::Before
}),
},
styles: vec![StyleDecl {
style: Style::Content(PseudoContent {
text: content.into(),
}),
importance: Importance::Default,
}],
}
}
pub fn do_decorate(mut self) -> Self {
self.style.add_agent_rules(&[
Self::make_surround_rule("em", false, "*"),
Self::make_surround_rule("em", true, "*"),
Self::make_surround_rule("dt", false, "*"),
Self::make_surround_rule("dt", true, "*"),
Self::make_surround_rule("strong", false, "**"),
Self::make_surround_rule("strong", true, "**"),
Self::make_surround_rule("b", false, "**"),
Self::make_surround_rule("b", true, "**"),
Self::make_surround_rule("code", false, "`"),
Self::make_surround_rule("code", true, "`"),
]);
self
}
pub fn link_footnotes(mut self, include_footnotes: bool) -> Self {
self.include_link_footnotes = include_footnotes;
self
}
pub fn empty_img_mode(mut self, img_mode: ImageRenderMode) -> Self {
self.image_mode = img_mode;
self
}
#[cfg(feature = "xml")]
pub fn xml_mode(mut self, xml_mode: XmlMode) -> Self {
self.xml_mode = xml_mode;
self
}
#[cfg(feature = "css_ext")]
pub fn register_highlighter(
mut self,
name: impl Into<String>,
f: SyntaxHighlighter,
) -> Self {
use std::rc::Rc;
self.syntax_highlighters.insert(name.into(), Rc::new(f));
self
}
}
impl Config<RichDecorator> {
pub fn coloured<R, FMap>(self, input: R, width: usize, colour_map: FMap) -> Result<String>
where
R: std::io::Read,
FMap: Fn(&[RichAnnotation], &str) -> String,
{
let mut context = self.make_context();
let render_tree = self.do_parse(&mut context, input)?;
self.render_coloured(render_tree, width, colour_map)
}
pub fn render_coloured<FMap>(
&self,
render_tree: RenderTree,
width: usize,
colour_map: FMap,
) -> Result<String>
where
FMap: Fn(&[RichAnnotation], &str) -> String,
{
let lines = self.render_to_lines(render_tree, width)?;
let mut result = String::new();
for line in lines {
for ts in line.tagged_strings() {
result.push_str(&colour_map(&ts.tag, &ts.s));
}
result.push('\n');
}
Ok(result)
}
}
pub fn rich() -> Config<RichDecorator> {
with_decorator(RichDecorator::new())
}
pub fn plain() -> Config<PlainDecorator> {
with_decorator(PlainDecorator::new())
.do_decorate()
.link_footnotes(true)
}
pub fn plain_no_decorate() -> Config<PlainDecorator> {
with_decorator(PlainDecorator::new())
}
pub fn with_decorator<D: TextDecorator>(decorator: D) -> Config<D> {
Config {
decorator,
style: Default::default(),
#[cfg(feature = "css")]
use_doc_css: false,
max_wrap_width: None,
pad_block_width: false,
allow_width_overflow: false,
min_wrap_width: MIN_WIDTH,
raw: false,
draw_borders: true,
wrap_links: true,
include_link_footnotes: false,
use_unicode_strikeout: true,
image_mode: ImageRenderMode::IgnoreEmpty,
#[cfg(feature = "xml")]
xml_mode: XmlMode::Auto,
#[cfg(feature = "css_ext")]
syntax_highlighters: Default::default(),
}
}
}
#[derive(Clone, Debug)]
pub struct RenderTree(RenderNode);
impl std::fmt::Display for RenderTree {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Render tree:")?;
self.0.write_self(f, 1)
}
}
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 render_options = RenderOptions {
wrap_width: context.max_wrap_width,
pad_block_width: context.pad_block_width,
allow_width_overflow: context.allow_width_overflow,
raw: context.raw,
draw_borders: context.draw_borders,
wrap_links: context.wrap_links,
include_link_footnotes: context.include_link_footnotes,
use_unicode_strikeout: context.use_unicode_strikeout,
img_mode: context.image_mode,
};
let test_decorator = decorator.make_subblock_decorator();
let builder = SubRenderer::new(width, render_options, decorator);
let builder =
render_tree_to_string(context, builder, &test_decorator, self.0, &mut io::sink())?;
Ok(RenderedText(builder))
}
}
struct RenderedText<D: TextDecorator>(SubRenderer<D>);
impl<D: TextDecorator> RenderedText<D> {
fn into_string(self) -> render::Result<String> {
self.0.into_string()
}
fn into_lines(self) -> Result<Vec<TaggedLine<Vec<D::Annotation>>>> {
Ok(self
.0
.into_lines()?
.into_iter()
.map(RenderLine::into_tagged_line)
.collect())
}
}
pub fn parse(input: impl io::Read) -> Result<RenderTree> {
let cfg = config::with_decorator(TrivialDecorator::new());
cfg.do_parse(&mut cfg.make_context(), input)
}
pub fn from_read_with_decorator<R, D>(input: R, width: usize, decorator: D) -> Result<String>
where
R: io::Read,
D: TextDecorator,
{
config::with_decorator(decorator).string_from_read(input, width)
}
pub fn from_read<R>(input: R, width: usize) -> Result<String>
where
R: io::Read,
{
config::plain().string_from_read(input, width)
}
pub fn from_read_rich<R>(input: R, width: usize) -> Result<Vec<TaggedLine<Vec<RichAnnotation>>>>
where
R: io::Read,
{
config::rich().lines_from_read(input, width)
}
mod ansi_colours;
pub use ansi_colours::from_read_coloured;
#[cfg(test)]
mod tests;
fn calc_ol_prefix_size<D: TextDecorator>(start: i64, num_items: usize, decorator: &D) -> usize {
let min_number = start;
let max_number = start + (num_items as i64) - 1;
let prefix_width_min = decorator.ordered_item_prefix(min_number).len();
let prefix_width_max = decorator.ordered_item_prefix(max_number).len();
max(prefix_width_min, prefix_width_max)
}