xh/src/formatting.rs
Jan Verbeek 00bc6f2238 Decode headers as latin1/UTF-8, show real reason phrase
External changes:

- We now print the actual reason phrase sent by the server instead
  of guessing it from the status code. That is, if servers reply with
  "200 Wonderful" instead of "200 OK" then we show that. This is
  especially useful for status codes that xh doesn't recognize.

- Header values are now decoded as latin1, with the UTF-8 decoding
  also shown if applicable.

- A new FAQ file with an entry that explains header value encoding.
  Header output now hyperlinks to this entry when relevant and if
  supported by the terminal.

Under the hood we now color headers manually. It's still hooked up to
the `.tmTheme` files but not to the `.sublime-syntax` file. This lets
us highlight the latin1 header values differently. In the future we
could use the same approach to optimize JSON highlighting.

I'm unsure about the position of the hyperlink. Currently it's the
text "UTF-8" in `<latin1 value> (UTF-8: <utf-8 value>)`. But that
means it's only shown if the value can be decoded as UTF-8. An
alternative is to turn the latin1 value itself into a hyperlink, but
that's confusing if the value itself is already a URL (which is a
common case for the `Location` header).

I also don't feel that our text is quite distinct enough from the
header value in the default `ansi` theme. Though the hyperlink does
help to set it apart.
2024-07-04 21:34:52 +02:00

163 lines
6.0 KiB
Rust

use std::{
io::{self, Write},
sync::OnceLock,
};
use syntect::dumps::from_binary;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
use termcolor::WriteColor;
use crate::{buffer::Buffer, cli::Theme};
pub(crate) mod headers;
pub(crate) mod palette;
pub fn get_json_formatter(indent_level: usize) -> jsonxf::Formatter {
let mut fmt = jsonxf::Formatter::pretty_printer();
fmt.indent = " ".repeat(indent_level);
fmt.record_separator = String::from("\n\n");
fmt.eager_record_separators = true;
fmt
}
/// Format a JSON value using serde. Unlike jsonxf this decodes escaped Unicode values.
///
/// Note that if parsing fails this function will stop midway through and return an error.
/// It should only be used with known-valid JSON.
pub fn serde_json_format(indent_level: usize, text: &str, write: impl Write) -> io::Result<()> {
let indent = " ".repeat(indent_level);
let formatter = serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes());
let mut serializer = serde_json::Serializer::with_formatter(write, formatter);
let mut deserializer = serde_json::Deserializer::from_str(text);
serde_transcode::transcode(&mut deserializer, &mut serializer)?;
Ok(())
}
pub(crate) static THEMES: once_cell::sync::Lazy<ThemeSet> = once_cell::sync::Lazy::new(|| {
from_binary(include_bytes!(concat!(
env!("OUT_DIR"),
"/themepack.themedump"
)))
});
static PS_BASIC: once_cell::sync::Lazy<SyntaxSet> = once_cell::sync::Lazy::new(|| {
from_binary(include_bytes!(concat!(env!("OUT_DIR"), "/basic.packdump")))
});
static PS_LARGE: once_cell::sync::Lazy<SyntaxSet> = once_cell::sync::Lazy::new(|| {
from_binary(include_bytes!(concat!(env!("OUT_DIR"), "/large.packdump")))
});
pub struct Highlighter<'a> {
highlighter: HighlightLines<'static>,
syntax_set: &'static SyntaxSet,
out: &'a mut Buffer,
}
/// A wrapper around a [`Buffer`] to add syntax highlighting when printing.
impl<'a> Highlighter<'a> {
pub fn new(syntax: &'static str, theme: Theme, out: &'a mut Buffer) -> Self {
let syntax_set: &SyntaxSet = match syntax {
"json" => &PS_BASIC,
_ => &PS_LARGE,
};
let syntax = syntax_set
.find_syntax_by_extension(syntax)
.expect("syntax not found");
Self {
highlighter: HighlightLines::new(syntax, theme.as_syntect_theme()),
syntax_set,
out,
}
}
/// Write a single piece of highlighted text.
/// May return a [`io::ErrorKind::Other`] when there is a problem
/// during highlighting.
pub fn highlight(&mut self, text: &str) -> io::Result<()> {
for line in LinesWithEndings::from(text) {
for (style, component) in self
.highlighter
.highlight_line(line, self.syntax_set)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?
{
self.out.set_color(&convert_style(style))?;
write!(self.out, "{}", component)?;
}
}
Ok(())
}
pub fn highlight_bytes(&mut self, line: &[u8]) -> io::Result<()> {
self.highlight(&String::from_utf8_lossy(line))
}
pub fn flush(&mut self) -> io::Result<()> {
self.out.flush()
}
}
impl Drop for Highlighter<'_> {
fn drop(&mut self) {
// This is just a best-effort attempt to restore the terminal, failure can be ignored
let _ = self.out.reset();
}
}
fn convert_style(style: syntect::highlighting::Style) -> termcolor::ColorSpec {
use syntect::highlighting::FontStyle;
let mut spec = termcolor::ColorSpec::new();
spec.set_fg(convert_color(style.foreground))
.set_underline(style.font_style.contains(FontStyle::UNDERLINE))
.set_bold(style.font_style.contains(FontStyle::BOLD))
.set_italic(style.font_style.contains(FontStyle::ITALIC));
spec
}
// https://github.com/sharkdp/bat/blob/3a85fd767bd1f03debd0a60ac5bc08548f95bc9d/src/terminal.rs
fn convert_color(color: syntect::highlighting::Color) -> Option<termcolor::Color> {
use termcolor::Color;
if color.a == 0 {
// Themes can specify one of the user-configurable terminal colors by
// encoding them as #RRGGBBAA with AA set to 00 (transparent) and RR set
// to the 8-bit color palette number. The built-in themes ansi-light,
// ansi-dark, base16, and base16-256 use this.
match color.r {
// For the first 7 colors, use the Color enum to produce ANSI escape
// sequences using codes 30-37 (foreground) and 40-47 (background).
// For example, red foreground is \x1b[31m. This works on terminals
// without 256-color support.
0x00 => Some(Color::Black),
0x01 => Some(Color::Red),
0x02 => Some(Color::Green),
0x03 => Some(Color::Yellow),
0x04 => Some(Color::Blue),
0x05 => Some(Color::Magenta),
0x06 => Some(Color::Cyan),
// The 8th color is white. Themes use it as the default foreground
// color, but that looks wrong on terminals with a light background.
// So keep that text uncolored instead.
0x07 => None,
// For all other colors, produce escape sequences using
// codes 38;5 (foreground) and 48;5 (background). For example,
// bright red foreground is \x1b[38;5;9m. This only works on
// terminals with 256-color support.
n => Some(Color::Ansi256(n)),
}
} else {
Some(Color::Rgb(color.r, color.g, color.b))
}
}
pub(crate) fn supports_hyperlinks() -> bool {
static SUPPORTS_HYPERLINKS: OnceLock<bool> = OnceLock::new();
*SUPPORTS_HYPERLINKS.get_or_init(supports_hyperlinks::supports_hyperlinks)
}
pub(crate) fn create_hyperlink(text: &str, url: &str) -> String {
// https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
format!("\x1B]8;;{url}\x1B\\{text}\x1B]8;;\x1B\\")
}