diff --git a/README.md b/README.md index 1889cf81..604ec1f6 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,13 @@ let config = codespan_reporting::term::Config::default(); term::emit_to_write_style(&mut writer.lock(), &config, &files, &diagnostic)?; +// For HTML output (e.g. embedding in SVG), use `HtmlWriter`. Access the inner +// buffer with `get_ref()` / `get_mut()`, and recover it with `into_inner()`. +let mut html = term::HtmlWriter::new(Vec::new()); +term::emit_to_write_style(&mut html, &config, &files, &diagnostic)?; +let num_lines = html.get_ref().iter().filter(|&&b| b == b'\n').count() + 1; +let _html_bytes = html.into_inner()?; + Ok::<(), codespan_reporting::files::Error>(()) ``` diff --git a/codespan-reporting/examples/readme_preview.rs b/codespan-reporting/examples/readme_preview.rs index 9f1cab90..79409fc5 100644 --- a/codespan-reporting/examples/readme_preview.rs +++ b/codespan-reporting/examples/readme_preview.rs @@ -7,10 +7,11 @@ //! cargo run --example readme_preview svg > codespan-reporting/assets/readme_preview.svg //! ``` -use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity}; +use std::io::Write; + +use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::files::SimpleFile; -use codespan_reporting::term::{self, Config}; -use codespan_reporting::term::{GeneralWrite, GeneralWriteResult}; +use codespan_reporting::term::{self, Config, HtmlWriter}; #[cfg(feature = "termcolor")] use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; @@ -99,14 +100,16 @@ fn main() -> anyhow::Result<()> { match parse_args()? { Opts::Svg => { - let mut writer = SvgWriter::new(); let config = Config::default(); + let mut writer = HtmlWriter::new(Vec::new()); for diagnostic in &diagnostics { term::emit_to_write_style(&mut writer, &config, &file, diagnostic)?; } - let num_lines = writer.line_count(); + let num_lines = writer.get_ref().iter().filter(|&&b| b == b'\n').count() + 1; + let buffer = writer.into_inner()?; + let content = String::from_utf8(buffer)?; let padding = 10; let font_size = 12; @@ -114,8 +117,6 @@ fn main() -> anyhow::Result<()> { let width = 882; let height = padding + num_lines * (font_size + line_spacing) + padding; - let content = writer.into_string(); - let stdout = std::io::stdout(); let writer = &mut stdout.lock(); @@ -217,156 +218,3 @@ fn main() -> anyhow::Result<()> { Ok(()) } - -// This whole example requires the std feature, but below is feature agnostic for reference - -#[cfg(feature = "std")] -type WriterBuffer = Vec; - -#[cfg(not(feature = "std"))] -type WriterBuffer = String; - -pub struct SvgWriter { - buffer: WriterBuffer, - span_open: bool, -} - -impl SvgWriter { - pub fn new() -> Self { - SvgWriter { - buffer: WriterBuffer::default(), - span_open: false, - } - } - - #[cfg(feature = "std")] - pub fn into_string(self) -> String { - String::from_utf8(self.buffer).unwrap() - } - - #[cfg(not(feature = "std"))] - pub fn into_string(self) -> String { - self.buffer - } - - #[cfg(feature = "std")] - pub fn line_count(&self) -> usize { - self.buffer.iter().filter(|byte| **byte == b'\n').count() + 1 - } - - #[cfg(not(feature = "std"))] - pub fn line_count(&self) -> usize { - self.buffer.lines().count() - } - - /// Close any open span - fn close_span(&mut self) -> GeneralWriteResult { - if self.span_open { - write!(self.buffer, "")?; - self.span_open = false; - } - Ok(()) - } - - /// Open a new span with the given CSS class - fn open_span(&mut self, class: &str) -> GeneralWriteResult { - // close existing first - self.close_span()?; - write!(self.buffer, "", class)?; - self.span_open = true; - Ok(()) - } -} - -#[cfg(feature = "std")] -impl std::io::Write for SvgWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let mut last = 0; - for (i, &b) in buf.iter().enumerate() { - let escape = match b { - b'<' => b"<"[..].as_ref(), - b'>' => b">"[..].as_ref(), - b'&' => b"&"[..].as_ref(), - _ => continue, - }; - self.buffer.write_all(&buf[last..i])?; - self.buffer.write_all(escape)?; - last = i + 1; - } - self.buffer.write_all(&buf[last..])?; - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.buffer.flush() - } -} - -#[cfg(not(feature = "std"))] -impl core::fmt::Write for SvgWriter { - fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { - let mut last = 0; - // TODO match indices - for (i, b) in s.chars().enumerate() { - let escape = match b { - '<' => "<", - '>' => ">", - '&' => "&", - _ => continue, - }; - self.buffer.write_str(&s[last..i])?; - self.buffer.write_str(escape)?; - last = i + 1; - } - self.buffer.write_str(&s[last..])?; - Ok(()) - } -} - -impl codespan_reporting::term::WriteStyle for SvgWriter { - fn set_header(&mut self, severity: Severity) -> GeneralWriteResult { - let class = match severity { - Severity::Bug => "header-bug", - Severity::Error => "header-error", - Severity::Warning => "header-warning", - Severity::Note => "header-note", - Severity::Help => "header-help", - }; - self.open_span(class) - } - - fn set_header_message(&mut self) -> GeneralWriteResult { - self.open_span("header-message") - } - - fn set_line_number(&mut self) -> GeneralWriteResult { - self.open_span("line-number") - } - - fn set_note_bullet(&mut self) -> GeneralWriteResult { - self.open_span("note-bullet") - } - - fn set_source_border(&mut self) -> GeneralWriteResult { - self.open_span("source-border") - } - - fn set_label(&mut self, severity: Severity, label_style: LabelStyle) -> GeneralWriteResult { - let sev = match severity { - Severity::Bug => "bug", - Severity::Error => "error", - Severity::Warning => "warning", - Severity::Note => "note", - Severity::Help => "help", - }; - let typ = match label_style { - LabelStyle::Primary => "primary", - LabelStyle::Secondary => "secondary", - }; - self.open_span(&format!("label-{}-{}", typ, sev)) - } - - fn reset(&mut self) -> GeneralWriteResult { - self.close_span() - } -} diff --git a/codespan-reporting/src/term/html.rs b/codespan-reporting/src/term/html.rs new file mode 100644 index 00000000..1b22e2a7 --- /dev/null +++ b/codespan-reporting/src/term/html.rs @@ -0,0 +1,156 @@ +//! HTML writer for emitting diagnostics as styled HTML. + +#[cfg(not(feature = "std"))] +use alloc::format; + +use crate::diagnostic::{LabelStyle, Severity}; + +use super::renderer::{GeneralWrite, GeneralWriteResult, WriteStyle}; + +/// Writer that emits diagnostics as HTML with `` for styling. +pub struct HtmlWriter { + upstream: W, + span_open: bool, +} + +impl HtmlWriter { + pub fn new(upstream: W) -> Self { + HtmlWriter { + upstream, + span_open: false, + } + } + + /// Get a reference to the underlying writer. + pub fn get_ref(&self) -> &W { + &self.upstream + } + + /// Get a mutable reference to the underlying writer. + pub fn get_mut(&mut self) -> &mut W { + &mut self.upstream + } + + /// Return the underlying writer, closing any open span first. + #[cfg(feature = "std")] + pub fn into_inner(mut self) -> std::io::Result { + self.close_span()?; + Ok(self.upstream) + } + + /// Return the underlying writer, closing any open span first. + #[cfg(not(feature = "std"))] + pub fn into_inner(mut self) -> Result { + self.close_span()?; + Ok(self.upstream) + } + + /// Close any open span + fn close_span(&mut self) -> GeneralWriteResult { + if self.span_open { + write!(self.upstream, "")?; + self.span_open = false; + } + Ok(()) + } + + /// Open a new span with the given CSS class + fn open_span(&mut self, class: &str) -> GeneralWriteResult { + self.close_span()?; + write!(self.upstream, "", class)?; + self.span_open = true; + Ok(()) + } +} + +#[cfg(feature = "std")] +impl std::io::Write for HtmlWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let mut last = 0; + for (i, &b) in buf.iter().enumerate() { + let escape = match b { + b'<' => b"<"[..].as_ref(), + b'>' => b">"[..].as_ref(), + b'&' => b"&"[..].as_ref(), + _ => continue, + }; + self.upstream.write_all(&buf[last..i])?; + self.upstream.write_all(escape)?; + last = i + 1; + } + self.upstream.write_all(&buf[last..])?; + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.upstream.flush() + } +} + +#[cfg(not(feature = "std"))] +impl core::fmt::Write for HtmlWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + let mut last = 0; + for (i, c) in s.char_indices() { + let escape = match c { + '<' => "<", + '>' => ">", + '&' => "&", + _ => continue, + }; + self.upstream.write_str(&s[last..i])?; + self.upstream.write_str(escape)?; + last = i + c.len_utf8(); + } + self.upstream.write_str(&s[last..])?; + Ok(()) + } +} + +impl WriteStyle for HtmlWriter { + fn set_header(&mut self, severity: Severity) -> GeneralWriteResult { + let class = match severity { + Severity::Bug => "header-bug", + Severity::Error => "header-error", + Severity::Warning => "header-warning", + Severity::Note => "header-note", + Severity::Help => "header-help", + }; + self.open_span(class) + } + + fn set_header_message(&mut self) -> GeneralWriteResult { + self.open_span("header-message") + } + + fn set_line_number(&mut self) -> GeneralWriteResult { + self.open_span("line-number") + } + + fn set_note_bullet(&mut self) -> GeneralWriteResult { + self.open_span("note-bullet") + } + + fn set_source_border(&mut self) -> GeneralWriteResult { + self.open_span("source-border") + } + + fn set_label(&mut self, severity: Severity, label_style: LabelStyle) -> GeneralWriteResult { + let sev = match severity { + Severity::Bug => "bug", + Severity::Error => "error", + Severity::Warning => "warning", + Severity::Note => "note", + Severity::Help => "help", + }; + let typ = match label_style { + LabelStyle::Primary => "primary", + LabelStyle::Secondary => "secondary", + }; + self.open_span(&format!("label-{}-{}", typ, sev)) + } + + fn reset(&mut self) -> GeneralWriteResult { + self.close_span() + } +} diff --git a/codespan-reporting/src/term/mod.rs b/codespan-reporting/src/term/mod.rs index e2c5cc50..8feda1a3 100644 --- a/codespan-reporting/src/term/mod.rs +++ b/codespan-reporting/src/term/mod.rs @@ -4,6 +4,7 @@ use crate::diagnostic::Diagnostic; use crate::files::Files; mod config; +mod html; mod renderer; mod views; @@ -15,6 +16,7 @@ pub use config::{Chars, Config, DisplayStyle}; // re-export #[cfg(feature = "termcolor")] pub use config::styles::{termcolor, Styles, StylesWriter}; +pub use html::HtmlWriter; pub use renderer::{GeneralWrite, GeneralWriteResult, Renderer, WriteStyle}; pub use views::{RichDiagnostic, ShortDiagnostic};