use super::{HtmlWriteError, HtmlWriteResult, HtmlWriterOptions};
#[cfg(feature = "gfm")]
use crate::ast::TableAlignment;
use crate::ast::{CodeBlockType, CustomNode, HeadingType, HtmlElement, ListItem, Node};
use crate::writer::runtime::diagnostics::{Diagnostic, DiagnosticSink, NullSink};
use crate::writer::runtime::visitor::{walk_node, NodeHandler};
use ecow::EcoString;
use html_escape;
use log;
use std::fmt;
mod guard;
pub(crate) use guard::GuardedHtmlElement;
use guard::GuardedTagWriter;
pub struct HtmlWriter {
pub options: HtmlWriterOptions,
pub(crate) buffer: EcoString,
tag_opened: bool,
diagnostics: Box<dyn DiagnosticSink + 'static>,
}
impl fmt::Debug for HtmlWriter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HtmlWriter")
.field("options", &self.options)
.field("buffer", &self.buffer)
.field("tag_opened", &self.tag_opened)
.finish()
}
}
impl Default for HtmlWriter {
fn default() -> Self {
Self::new()
}
}
impl HtmlWriter {
pub fn new() -> Self {
Self::with_options(HtmlWriterOptions::default())
}
pub fn with_options(options: HtmlWriterOptions) -> Self {
HtmlWriter {
options,
buffer: EcoString::new(),
tag_opened: false,
diagnostics: Box::new(NullSink),
}
}
pub fn with_diagnostic_sink(mut self, sink: Box<dyn DiagnosticSink + 'static>) -> Self {
self.diagnostics = sink;
self
}
pub fn set_diagnostic_sink(&mut self, sink: Box<dyn DiagnosticSink + 'static>) {
self.diagnostics = sink;
}
pub fn diagnostic_sink(&mut self) -> &mut dyn DiagnosticSink {
self.diagnostics.as_mut()
}
pub(crate) fn emit_warning<S: Into<EcoString>>(&mut self, message: S) {
let message = message.into();
self.diagnostics.emit(Diagnostic::warning(message.clone()));
log::warn!("{message}");
}
#[allow(dead_code)]
pub(crate) fn emit_info<S: Into<EcoString>>(&mut self, message: S) {
let message = message.into();
self.diagnostics.emit(Diagnostic::info(message.clone()));
log::info!("{message}");
}
#[allow(dead_code)]
pub(crate) fn emit_debug<S: Into<EcoString>>(&mut self, message: S) {
let message = message.into();
self.diagnostics.emit(Diagnostic::info(message.clone()));
log::debug!("{message}");
}
pub fn set_options(&mut self, options: HtmlWriterOptions) {
self.options = options;
}
pub fn options(&self) -> &HtmlWriterOptions {
&self.options
}
pub fn options_mut(&mut self) -> &mut HtmlWriterOptions {
&mut self.options
}
pub fn with_modified_options<F>(mut self, f: F) -> Self
where
F: FnOnce(&mut HtmlWriterOptions),
{
f(&mut self.options);
self
}
pub fn into_string(mut self) -> HtmlWriteResult<EcoString> {
self.ensure_tag_closed()?;
Ok(self.buffer)
}
fn ensure_tag_closed(&mut self) -> HtmlWriteResult<()> {
if self.tag_opened {
self.buffer.push('>');
self.tag_opened = false;
}
Ok(())
}
pub fn start_tag(&mut self, tag_name: &str) -> HtmlWriteResult<()> {
self.ensure_tag_closed()?;
self.buffer.push('<');
self.buffer.push_str(tag_name);
self.tag_opened = true;
Ok(())
}
pub fn attribute(&mut self, key: &str, value: &str) -> HtmlWriteResult<()> {
if !self.tag_opened {
return Err(HtmlWriteError::InvalidHtmlTag(
"Cannot write attribute: no tag is currently open.".to_string(),
));
}
self.buffer.push(' ');
self.buffer.push_str(key);
self.buffer.push_str("=\"");
self.buffer
.push_str(html_escape::encode_double_quoted_attribute(value).as_ref());
self.buffer.push('"');
Ok(())
}
pub fn finish_tag(&mut self) -> HtmlWriteResult<()> {
if self.tag_opened {
self.buffer.push('>');
self.tag_opened = false;
}
Ok(())
}
pub fn end_tag(&mut self, tag_name: &str) -> HtmlWriteResult<()> {
self.ensure_tag_closed()?;
self.buffer.push_str("</");
self.buffer.push_str(tag_name);
self.buffer.push('>');
Ok(())
}
pub fn text(&mut self, text: &str) -> HtmlWriteResult<()> {
self.ensure_tag_closed()?;
self.buffer
.push_str(html_escape::encode_text(text).as_ref());
Ok(())
}
pub fn self_closing_tag(&mut self, tag_name: &str) -> HtmlWriteResult<()> {
self.ensure_tag_closed()?;
self.buffer.push('<');
self.buffer.push_str(tag_name);
self.buffer.push_str(" />");
self.tag_opened = false;
Ok(())
}
pub fn finish_self_closing_tag(&mut self) -> HtmlWriteResult<()> {
if !self.tag_opened {
return Err(HtmlWriteError::InvalidHtmlTag(
"Cannot finish self-closing tag: no tag is currently open.".to_string(),
));
}
self.buffer.push_str(" />");
self.tag_opened = false;
Ok(())
}
pub fn write_trusted_html(&mut self, html: &str) -> HtmlWriteResult<()> {
self.ensure_tag_closed()?;
self.buffer.push_str(html);
Ok(())
}
pub fn write_untrusted_html(&mut self, html: &str) -> HtmlWriteResult<()> {
self.text(html)
}
pub(crate) fn guard_html_element<'a>(
&'a mut self,
element: &HtmlElement,
) -> HtmlWriteResult<GuardedHtmlElement<'a>> {
#[cfg(feature = "gfm")]
if self.options.enable_gfm
&& self
.options
.gfm_disallowed_html_tags
.iter()
.any(|tag| tag.eq_ignore_ascii_case(&element.tag))
{
self.emit_debug(format!(
"GFM: Textualizing disallowed HTML tag: <{}>",
element.tag
));
return Ok(GuardedHtmlElement::Textualize);
}
if !crate::writer::html::utils::is_safe_tag_name(&element.tag) {
if self.options.strict {
return Err(HtmlWriteError::InvalidHtmlTag(element.tag.to_string()));
}
self.emit_warning(format!(
"Invalid HTML tag name '{}' encountered. Textualizing in non-strict mode.",
element.tag
));
return Ok(GuardedHtmlElement::Textualize);
}
for attr in &element.attributes {
if !crate::writer::html::utils::is_safe_attribute_name(&attr.name) {
if self.options.strict {
return Err(HtmlWriteError::InvalidHtmlAttribute(attr.name.to_string()));
}
self.emit_warning(format!(
"Invalid attribute name '{}' encountered. Textualizing element in non-strict mode.",
attr.name
));
return Ok(GuardedHtmlElement::Textualize);
}
}
self.start_tag(&element.tag)?;
Ok(GuardedHtmlElement::Render(GuardedTagWriter::new(
self,
element.tag.clone(),
)))
}
#[deprecated(
since = "0.8.0",
note = "Use write_trusted_html for trusted fragments or write_untrusted_html for escaping"
)]
pub fn raw_html(&mut self, html: &str) -> HtmlWriteResult<()> {
self.write_trusted_html(html)
}
pub fn write_node(&mut self, node: &Node) -> HtmlWriteResult<()> {
walk_node(self, node)
}
}
impl NodeHandler for HtmlWriter {
type Error = HtmlWriteError;
fn document(&mut self, children: &[Node]) -> HtmlWriteResult<()> {
self.write_document(children)
}
fn paragraph(&mut self, content: &[Node]) -> HtmlWriteResult<()> {
self.write_paragraph(content)
}
fn text(&mut self, text: &EcoString) -> HtmlWriteResult<()> {
self.write_text(text)
}
fn emphasis(&mut self, content: &[Node]) -> HtmlWriteResult<()> {
self.write_emphasis(content)
}
fn strong(&mut self, content: &[Node]) -> HtmlWriteResult<()> {
self.write_strong(content)
}
fn thematic_break(&mut self) -> HtmlWriteResult<()> {
self.write_thematic_break()
}
fn heading(
&mut self,
level: u8,
content: &[Node],
_heading_type: &HeadingType,
) -> HtmlWriteResult<()> {
self.write_heading(level, content)
}
fn inline_code(&mut self, code: &EcoString) -> HtmlWriteResult<()> {
self.write_inline_code(code)
}
fn code_block(
&mut self,
language: &Option<EcoString>,
content: &EcoString,
_kind: &CodeBlockType,
) -> HtmlWriteResult<()> {
self.write_code_block(language, content)
}
fn html_block(&mut self, content: &EcoString) -> HtmlWriteResult<()> {
self.write_html_block(content)
}
fn html_element(&mut self, element: &HtmlElement) -> HtmlWriteResult<()> {
self.write_html_element(element)
}
fn block_quote(&mut self, content: &[Node]) -> HtmlWriteResult<()> {
self.write_blockquote(content)
}
fn unordered_list(&mut self, items: &[ListItem]) -> HtmlWriteResult<()> {
self.write_unordered_list(items)
}
fn ordered_list(&mut self, start: u32, items: &[ListItem]) -> HtmlWriteResult<()> {
self.write_ordered_list(start, items)
}
#[cfg(feature = "gfm")]
fn table(
&mut self,
headers: &[Node],
alignments: &[TableAlignment],
rows: &[Vec<Node>],
) -> HtmlWriteResult<()> {
self.write_table(headers, alignments, rows)
}
#[cfg(not(feature = "gfm"))]
fn table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> HtmlWriteResult<()> {
self.write_table(headers, rows)
}
fn link(
&mut self,
url: &EcoString,
title: &Option<EcoString>,
content: &[Node],
) -> HtmlWriteResult<()> {
self.write_link(url, title, content)
}
fn image(
&mut self,
url: &EcoString,
title: &Option<EcoString>,
alt: &[Node],
) -> HtmlWriteResult<()> {
self.write_image(url, title, alt)
}
fn soft_break(&mut self) -> HtmlWriteResult<()> {
self.write_soft_break()
}
fn hard_break(&mut self) -> HtmlWriteResult<()> {
self.write_hard_break()
}
fn autolink(&mut self, url: &EcoString, is_email: bool) -> HtmlWriteResult<()> {
self.write_autolink(url, is_email)
}
#[cfg(feature = "gfm")]
fn extended_autolink(&mut self, url: &EcoString) -> HtmlWriteResult<()> {
self.write_extended_autolink(url)
}
fn link_reference_definition(
&mut self,
_label: &EcoString,
_destination: &EcoString,
_title: &Option<EcoString>,
) -> HtmlWriteResult<()> {
Ok(())
}
fn reference_link(&mut self, label: &EcoString, content: &[Node]) -> HtmlWriteResult<()> {
self.write_reference_link(label, content)
}
#[cfg(feature = "gfm")]
fn strikethrough(&mut self, content: &[Node]) -> HtmlWriteResult<()> {
self.write_strikethrough(content)
}
fn custom(&mut self, node: &dyn CustomNode) -> HtmlWriteResult<()> {
node.html_write(self)
}
fn unsupported(&mut self, node: &Node) -> HtmlWriteResult<()> {
#[cfg(not(feature = "gfm"))]
if let Node::ExtendedAutolink(url) = node {
self.emit_warning(
format!(
"ExtendedAutolink encountered but GFM feature is not enabled. Rendering as text: {url}"
),
);
return self.text(url);
}
Err(HtmlWriteError::UnsupportedNodeType(format!("{node:?}")))
}
}