mirror of
https://github.com/tree-sitter/tree-sitter.git
synced 2026-09-10 07:36:22 -04:00
feat(cli)!: rework HTML output modes in highlight
- `--layout <document|line-numbers|fragment>` (default `document`): the document structure. `document` is a self-contained page wrapping a plain `<div class="highlight"><pre><code>` block, `line-numbers` adds a line-number `<table>`, and `fragment` emits only the code markup with no surrounding `<head>`/`<style>`/`<body>` so it can be embedded in an existing page. - `--style <classes|inline|minimal>` (default `classes`): how token colors are applied. `classes` uses `class="..."` spans plus a generated `<style>`, `inline` bakes colors onto each span so the markup is self-contained, and `minimal` emits bare classes with no colors (bring your own stylesheet). Deprecates the `--css-classes` flag.
This commit is contained in:
parent
d205dc92de
commit
3a76a8c8ca
|
|
@ -12,6 +12,7 @@ use std::{
|
|||
use ansi_colours::{ansi256_from_rgb, rgb_from_ansi256};
|
||||
use anstyle::{Ansi256Color, AnsiColor, Color, Effects, RgbColor};
|
||||
use anyhow::Result;
|
||||
use clap::ValueEnum;
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeMap};
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -26,8 +27,9 @@ pub const HTML_HEAD_HEADER: &str = "
|
|||
<style>
|
||||
body {
|
||||
font-family: monospace
|
||||
}
|
||||
.line-number {
|
||||
}";
|
||||
|
||||
pub const HTML_LINE_NUMBER_STYLE: &str = " .line-number {
|
||||
user-select: none;
|
||||
text-align: right;
|
||||
color: rgba(27,31,35,.3);
|
||||
|
|
@ -35,8 +37,7 @@ pub const HTML_HEAD_HEADER: &str = "
|
|||
}
|
||||
.line {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>";
|
||||
}";
|
||||
|
||||
pub const HTML_BODY_HEADER: &str = "
|
||||
</head>
|
||||
|
|
@ -308,12 +309,36 @@ fn terminal_supports_truecolor() -> bool {
|
|||
.is_ok_and(|truecolor| truecolor == "truecolor" || truecolor == "24bit")
|
||||
}
|
||||
|
||||
/// The kind of HTML emitted when highlighting to HTML.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
pub enum HtmlOutput {
|
||||
/// A complete, self-contained document wrapping a plain
|
||||
/// `<div class="highlight"><pre><code>` block.
|
||||
Document,
|
||||
/// A complete document with a line-number column (a `<table>` layout).
|
||||
#[value(name = "line-numbers")]
|
||||
NumberedDocument,
|
||||
/// Only the code markup, without the surrounding document.
|
||||
Fragment,
|
||||
}
|
||||
|
||||
/// How token colors are applied in HTML output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
pub enum HtmlStyling {
|
||||
/// `class="..."` spans plus a generated `<style>` carrying the theme's colors.
|
||||
Classes,
|
||||
/// `style="..."` spans with the colors inlined.
|
||||
Inline,
|
||||
/// `class="..."` spans with no colors emitted (supply your own stylesheet).
|
||||
Minimal,
|
||||
}
|
||||
|
||||
pub struct HighlightOptions {
|
||||
pub theme: Theme,
|
||||
pub check: bool,
|
||||
pub captures_path: Option<PathBuf>,
|
||||
pub inline_styles: bool,
|
||||
pub html: bool,
|
||||
/// `None` for regular output, `Some((layout, style))` when emitting HTML.
|
||||
pub html: Option<(HtmlOutput, HtmlStyling)>,
|
||||
pub quiet: bool,
|
||||
pub print_time: bool,
|
||||
pub cancellation_flag: Arc<AtomicUsize>,
|
||||
|
|
@ -395,19 +420,25 @@ pub fn highlight(
|
|||
)?;
|
||||
let theme = &opts.theme;
|
||||
|
||||
if !opts.quiet && print_name {
|
||||
// A fragment is pure code markup, so it must not be prefixed with the filename.
|
||||
let html_fragment = opts
|
||||
.html
|
||||
.is_some_and(|(layout, _)| layout == HtmlOutput::Fragment);
|
||||
if !opts.quiet && print_name && !html_fragment {
|
||||
writeln!(&mut stdout, "{name}")?;
|
||||
}
|
||||
|
||||
if opts.html {
|
||||
if !opts.quiet {
|
||||
if let Some((layout, style)) = opts.html {
|
||||
if !opts.quiet && layout != HtmlOutput::Fragment {
|
||||
writeln!(&mut stdout, "{HTML_HEAD_HEADER}")?;
|
||||
writeln!(&mut stdout, " <style>")?;
|
||||
let names = theme.highlight_names.iter();
|
||||
let styles = theme.styles.iter();
|
||||
for (name, style) in names.zip(styles) {
|
||||
if let Some(css) = &style.css {
|
||||
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
|
||||
if layout == HtmlOutput::NumberedDocument {
|
||||
writeln!(&mut stdout, "{HTML_LINE_NUMBER_STYLE}")?;
|
||||
}
|
||||
if style == HtmlStyling::Classes {
|
||||
for (name, style) in theme.highlight_names.iter().zip(&theme.styles) {
|
||||
if let Some(css) = &style.css {
|
||||
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
writeln!(&mut stdout, " </style>")?;
|
||||
|
|
@ -416,7 +447,7 @@ pub fn highlight(
|
|||
|
||||
let mut renderer = HtmlRenderer::new();
|
||||
renderer.render(events, &source, &move |highlight, output| {
|
||||
if opts.inline_styles {
|
||||
if style == HtmlStyling::Inline {
|
||||
output.extend(b"style='");
|
||||
output.extend(
|
||||
theme.styles[highlight.0]
|
||||
|
|
@ -438,16 +469,29 @@ pub fn highlight(
|
|||
})?;
|
||||
|
||||
if !opts.quiet {
|
||||
writeln!(&mut stdout, "<table>")?;
|
||||
for (i, line) in renderer.lines().enumerate() {
|
||||
if layout == HtmlOutput::NumberedDocument {
|
||||
writeln!(&mut stdout, "<table>")?;
|
||||
for (i, line) in renderer.lines().enumerate() {
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
|
||||
i + 1,
|
||||
)?;
|
||||
}
|
||||
writeln!(&mut stdout, "</table>")?;
|
||||
} else {
|
||||
let mut body = renderer.lines().collect::<String>();
|
||||
if body.ends_with('\n') {
|
||||
body.pop();
|
||||
}
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
|
||||
i + 1,
|
||||
"<div class=\"highlight\">\n<pre><code>{body}</code></pre>\n</div>",
|
||||
)?;
|
||||
}
|
||||
writeln!(&mut stdout, "</table>")?;
|
||||
writeln!(&mut stdout, "{HTML_FOOTER}")?;
|
||||
if layout != HtmlOutput::Fragment {
|
||||
writeln!(&mut stdout, "{HTML_FOOTER}")?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut style_stack = vec![theme.default_style().ansi];
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use tree_sitter_cli::{
|
|||
DEFAULT_EDIT_COUNT, DEFAULT_ITERATION_COUNT, EDIT_COUNT, FuzzOptions, ITERATION_COUNT,
|
||||
LOG_ENABLED, LOG_GRAPH_ENABLED, START_SEED, fuzz_language_corpus,
|
||||
},
|
||||
highlight::{self, HighlightOptions},
|
||||
highlight::{self, HighlightOptions, HtmlOutput, HtmlStyling},
|
||||
init::{JsonConfigOpts, TREE_SITTER_JSON_SCHEMA, generate_grammar_files},
|
||||
input::{CliInput, get_input, get_tmp_source_file},
|
||||
logger, paint,
|
||||
|
|
@ -508,9 +508,15 @@ struct Highlight {
|
|||
/// Generate highlighting as an HTML document
|
||||
#[arg(long, short = 'H')]
|
||||
pub html: bool,
|
||||
/// When generating HTML, use css classes rather than inline styles
|
||||
#[arg(long, requires = "html")]
|
||||
/// Deprecated: use `--style classes`
|
||||
#[arg(long, requires = "html", conflicts_with = "style")]
|
||||
pub css_classes: bool,
|
||||
/// When generating HTML, the document structure to emit
|
||||
#[arg(long, requires = "html", value_enum, default_value = "document")]
|
||||
pub layout: HtmlOutput,
|
||||
/// When generating HTML, how token colors are applied
|
||||
#[arg(long, requires = "html", value_enum, default_value = "classes")]
|
||||
pub style: HtmlStyling,
|
||||
/// Check that highlighting captures conform strictly to standards
|
||||
#[arg(long)]
|
||||
pub check: bool,
|
||||
|
|
@ -1704,12 +1710,19 @@ impl Highlight {
|
|||
Encoding::Utf16BE => ffi::TSInputEncodingUTF16BE,
|
||||
});
|
||||
|
||||
let style = if self.css_classes {
|
||||
// TODO: Remove during the 0.28 release cycle
|
||||
warn!("--css-classes is deprecated, use --style classes instead");
|
||||
HtmlStyling::Classes
|
||||
} else {
|
||||
self.style
|
||||
};
|
||||
|
||||
let options = HighlightOptions {
|
||||
theme: theme_config.theme,
|
||||
check: self.check,
|
||||
captures_path: self.captures_path,
|
||||
inline_styles: !self.css_classes,
|
||||
html: self.html,
|
||||
html: self.html.then_some((self.layout, style)),
|
||||
quiet: self.quiet,
|
||||
print_time: self.time,
|
||||
cancellation_flag: cancellation_flag.clone(),
|
||||
|
|
|
|||
|
|
@ -14,9 +14,23 @@ tree-sitter highlight [OPTIONS] [PATHS]... # Aliases: hi
|
|||
|
||||
Output an HTML document with syntax highlighting.
|
||||
|
||||
### `--css-classes`
|
||||
### `--layout <LAYOUT>`
|
||||
|
||||
Output HTML with CSS classes instead of inline styles.
|
||||
When generating HTML, the document structure to emit. One of:
|
||||
|
||||
- `document` (default): a complete, self-contained HTML document wrapping a plain `<div class="highlight"><pre><code>`
|
||||
block.
|
||||
- `line-numbers`: a complete document with a line-number column (a `<table>` layout).
|
||||
- `fragment`: only the code markup, without the surrounding `<head>`/`<style>`/`<body>` document, so it can be embedded
|
||||
in an existing page.
|
||||
|
||||
### `--style <STYLE>`
|
||||
|
||||
When generating HTML, how token colors are applied. One of:
|
||||
|
||||
- `classes` (default): `class="..."` spans plus a generated `<style>` block carrying the theme's colors.
|
||||
- `inline`: `style="..."` spans with the colors inlined, so the markup is self-contained.
|
||||
- `minimal`: `class="..."` spans with no colors emitted (supply your own stylesheet).
|
||||
|
||||
### `--check`
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue