diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a9053f..7833e2fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased: mitmproxy_rs next +- Add JS syntax highlighting. +- Add CSS syntax highlighting. ## 30 May 2025: mitmproxy_rs 0.12.5 diff --git a/Cargo.lock b/Cargo.lock index a9061596..14146b87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2260,7 +2260,9 @@ dependencies = [ "anyhow", "criterion", "tree-sitter", + "tree-sitter-css", "tree-sitter-highlight", + "tree-sitter-javascript", "tree-sitter-xml", "tree-sitter-yaml", ] @@ -3762,6 +3764,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-css" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad6489794d41350d12a7fbe520e5199f688618f43aace5443980d1ddcf1b29e" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-highlight" version = "0.25.5" @@ -3774,6 +3786,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-javascript" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-language" version = "0.1.5" diff --git a/mitmproxy-highlight/Cargo.toml b/mitmproxy-highlight/Cargo.toml index 5e8c41a2..f08f4305 100644 --- a/mitmproxy-highlight/Cargo.toml +++ b/mitmproxy-highlight/Cargo.toml @@ -13,14 +13,16 @@ workspace = true [dependencies] anyhow = { version = "1.0.97", features = ["backtrace"] } +tree-sitter = "0.25.5" +tree-sitter-css = "0.23.2" tree-sitter-highlight = "0.25.5" -tree-sitter-yaml = "0.7.1" +tree-sitter-javascript = "0.23.1" tree-sitter-xml = "0.7.0" -tree-sitter = "0.25.5" +tree-sitter-yaml = "0.7.1" [dev-dependencies] criterion = "0.6.0" [[bench]] name = "syntax_highlight" -harness = false \ No newline at end of file +harness = false diff --git a/mitmproxy-highlight/src/common.rs b/mitmproxy-highlight/src/common.rs index d1225639..f84f5135 100644 --- a/mitmproxy-highlight/src/common.rs +++ b/mitmproxy-highlight/src/common.rs @@ -39,7 +39,7 @@ pub fn highlight( } #[cfg(test)] -pub(super) fn test_names_ok( +pub(super) fn test_tags_ok( language: tree_sitter::Language, highlights_query: &str, names: &[&str], diff --git a/mitmproxy-highlight/src/css.rs b/mitmproxy-highlight/src/css.rs new file mode 100644 index 00000000..4f719975 --- /dev/null +++ b/mitmproxy-highlight/src/css.rs @@ -0,0 +1,92 @@ +use super::{common, Chunk, Tag}; +use anyhow::Result; +use std::sync::LazyLock; +use tree_sitter_css::HIGHLIGHTS_QUERY; +use tree_sitter_css::LANGUAGE; +use tree_sitter_highlight::HighlightConfiguration; + +const NAMES: &[&str] = &[ + "tag", // body + "property", // font-size + "variable", // --foo-bar + "function", // calc() + "number", // 42 + "string", // "foo" + "comment", // /* comment */ +]; +const TAGS: &[Tag] = &[ + Tag::Name, + Tag::Boolean, // we only have one "Name", so this is a workaround. + Tag::Text, + Tag::Text, + Tag::Number, + Tag::String, + Tag::Comment, +]; + +static CONFIG: LazyLock = LazyLock::new(|| { + let mut config = HighlightConfiguration::new(LANGUAGE.into(), "", HIGHLIGHTS_QUERY, "", "") + .expect("failed to build syntax highlighter"); + config.configure(NAMES); + config +}); + +pub fn highlight(input: &[u8]) -> Result> { + common::highlight(&CONFIG, TAGS, input) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[ignore] + #[test] + fn debug() { + common::debug( + LANGUAGE.into(), + HIGHLIGHTS_QUERY, + b"p > span { color: red; font-size: 42px; content: \"foo\"; margin: var(--foo) } /* foo */", + ); + } + + #[test] + fn test_tags_ok() { + common::test_tags_ok(LANGUAGE.into(), HIGHLIGHTS_QUERY, NAMES, TAGS); + } + + #[test] + fn test_highlight() { + let input = b"\ + p > span { \n\ + color: red;\n\ + font-size: 42px;\n\ + content: \"foo\";\n\ + margin: var(--foo);\n\ + }\n\ + /* foo */\n\ + "; + let chunks = highlight(input).unwrap(); + assert_eq!( + chunks, + vec![ + (Tag::Name, "p".to_string()), + (Tag::Text, " > ".to_string()), + (Tag::Name, "span".to_string()), + (Tag::Text, " { \n".to_string()), + (Tag::Boolean, "color".to_string()), + (Tag::Text, ": red;\n".to_string()), + (Tag::Boolean, "font-size".to_string()), + (Tag::Text, ": ".to_string()), + (Tag::Number, "42px".to_string()), + (Tag::Text, ";\n".to_string()), + (Tag::Boolean, "content".to_string()), + (Tag::Text, ": ".to_string()), + (Tag::String, "\"foo\"".to_string()), + (Tag::Text, ";\n".to_string()), + (Tag::Boolean, "margin".to_string()), + (Tag::Text, ": var(--foo);\n}\n".to_string()), + (Tag::Comment, "/* foo */\n".to_string()), + ] + ); + } +} diff --git a/mitmproxy-highlight/src/javascript.rs b/mitmproxy-highlight/src/javascript.rs new file mode 100644 index 00000000..8f585d51 --- /dev/null +++ b/mitmproxy-highlight/src/javascript.rs @@ -0,0 +1,84 @@ +use super::{common, Chunk, Tag}; +use anyhow::Result; +use std::sync::LazyLock; +use tree_sitter_highlight::HighlightConfiguration; +use tree_sitter_javascript::HIGHLIGHT_QUERY as HIGHLIGHTS_QUERY; +use tree_sitter_javascript::LANGUAGE; + +const NAMES: &[&str] = &[ + "keyword", // let + "function", // *function* () { + "variable", // let *foo* = ... + "property", // foo.*bar* = ... + "constant", // *true* + "string", // "string" + "number", // 42 + "comment", // /* comments */ +]; +const TAGS: &[Tag] = &[ + Tag::Name, + Tag::Text, + Tag::Text, + Tag::Text, + Tag::Boolean, + Tag::String, + Tag::Number, + Tag::Comment, +]; + +static CONFIG: LazyLock = LazyLock::new(|| { + let mut config = HighlightConfiguration::new(LANGUAGE.into(), "", HIGHLIGHTS_QUERY, "", "") + .expect("failed to build syntax highlighter"); + config.configure(NAMES); + config +}); + +pub fn highlight(input: &[u8]) -> Result> { + common::highlight(&CONFIG, TAGS, input) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[ignore] + #[test] + fn debug() { + common::debug( + LANGUAGE.into(), + HIGHLIGHTS_QUERY, + b"function foo() { let bar = true && 42 && 'qux'; foo.bar = 42; } // comment", + ); + } + + #[test] + fn test_tags_ok() { + common::test_tags_ok(LANGUAGE.into(), HIGHLIGHTS_QUERY, NAMES, TAGS); + } + + #[test] + fn test_highlight() { + let input = b"\ + function foo() {\n\ + let bar = true && 42 && 'qux';\n\ + } // comment\n\ + "; + let chunks = highlight(input).unwrap(); + assert_eq!( + chunks, + vec![ + (Tag::Name, "function ".to_string()), + (Tag::Text, "foo() {\n".to_string()), + (Tag::Name, "let ".to_string()), + (Tag::Text, "bar = ".to_string()), + (Tag::Boolean, "true".to_string()), + (Tag::Text, " && ".to_string()), + (Tag::Number, "42".to_string()), + (Tag::Text, " && ".to_string()), + (Tag::String, "'qux'".to_string()), + (Tag::Text, ";\n} ".to_string()), + (Tag::Comment, "// comment\n".to_string()), + ] + ); + } +} diff --git a/mitmproxy-highlight/src/lib.rs b/mitmproxy-highlight/src/lib.rs index 0cfa1526..f199c8ee 100644 --- a/mitmproxy-highlight/src/lib.rs +++ b/mitmproxy-highlight/src/lib.rs @@ -2,23 +2,29 @@ use anyhow::bail; use std::str::FromStr; pub mod common; +mod css; +mod javascript; mod xml; mod yaml; pub type Chunk = (Tag, String); pub enum Language { + Css, + JavaScript, Xml, Yaml, - Error, None, + Error, } impl Language { pub fn highlight(&self, input: &[u8]) -> anyhow::Result> { match self { - Language::Yaml => yaml::highlight_yaml(input), - Language::Xml => xml::highlight_xml(input), + Language::Css => css::highlight(input), + Language::JavaScript => javascript::highlight(input), + Language::Xml => xml::highlight(input), + Language::Yaml => yaml::highlight(input), Language::None => Ok(vec![( Tag::Text, String::from_utf8_lossy(input).to_string(), @@ -30,14 +36,23 @@ impl Language { } } - pub const VALUES: [Self; 4] = [Self::Xml, Self::Yaml, Self::Error, Self::None]; + pub const VALUES: [Self; 6] = [ + Self::Css, + Self::JavaScript, + Self::Xml, + Self::Yaml, + Self::None, + Self::Error, + ]; - pub fn as_str(&self) -> &'static str { + pub const fn as_str(&self) -> &'static str { match self { + Self::Css => "css", + Self::JavaScript => "javascript", Self::Xml => "xml", Self::Yaml => "yaml", - Self::Error => "error", Self::None => "none", + Self::Error => "error", } } } @@ -47,6 +62,8 @@ impl FromStr for Language { fn from_str(s: &str) -> Result { Ok(match s { + "css" => Language::Css, + "javascript" => Language::JavaScript, "xml" => Language::Xml, "yaml" => Language::Yaml, "none" => Language::None, diff --git a/mitmproxy-highlight/src/xml.rs b/mitmproxy-highlight/src/xml.rs index eb3ab939..6566f058 100644 --- a/mitmproxy-highlight/src/xml.rs +++ b/mitmproxy-highlight/src/xml.rs @@ -1,8 +1,9 @@ -use super::common::highlight; +use super::common; use super::{Chunk, Tag}; use anyhow::Result; use std::sync::LazyLock; use tree_sitter_highlight::HighlightConfiguration; +use tree_sitter_xml::{LANGUAGE_XML as LANGUAGE, XML_HIGHLIGHT_QUERY as HIGHLIGHTS_QUERY}; const NAMES: &[&str] = &[ "tag", //
@@ -21,52 +22,40 @@ const TAGS: &[Tag] = &[ Tag::Text, // markup ]; -static XML_CONFIG: LazyLock = LazyLock::new(|| { - let mut config = HighlightConfiguration::new( - tree_sitter_xml::LANGUAGE_XML.into(), - "", - tree_sitter_xml::XML_HIGHLIGHT_QUERY, - "", - "", - ) - .expect("failed to build XML syntax highlighter"); +static CONFIG: LazyLock = LazyLock::new(|| { + let mut config = HighlightConfiguration::new(LANGUAGE.into(), "", HIGHLIGHTS_QUERY, "", "") + .expect("failed to build syntax highlighter"); config.configure(NAMES); config }); -pub fn highlight_xml(input: &[u8]) -> Result> { - highlight(&XML_CONFIG, TAGS, input) +pub fn highlight(input: &[u8]) -> Result> { + common::highlight(&CONFIG, TAGS, input) } #[cfg(test)] mod tests { use super::*; - use crate::common; #[ignore] #[test] fn debug() { common::debug( - tree_sitter_xml::LANGUAGE_XML.into(), - tree_sitter_xml::XML_HIGHLIGHT_QUERY, + LANGUAGE.into(), + HIGHLIGHTS_QUERY, b"
Hello
", ); } #[test] fn test_tags_ok() { - common::test_names_ok( - tree_sitter_xml::LANGUAGE_XML.into(), - tree_sitter_xml::XML_HIGHLIGHT_QUERY, - NAMES, - TAGS, - ); + common::test_tags_ok(LANGUAGE.into(), HIGHLIGHTS_QUERY, NAMES, TAGS); } #[test] - fn test_highlight_xml() { + fn test_highlight() { let input = b"
Hello
"; - let chunks = highlight_xml(input).unwrap(); + let chunks = highlight(input).unwrap(); assert_eq!( chunks, vec![ diff --git a/mitmproxy-highlight/src/yaml.rs b/mitmproxy-highlight/src/yaml.rs index fe0b79d2..09520414 100644 --- a/mitmproxy-highlight/src/yaml.rs +++ b/mitmproxy-highlight/src/yaml.rs @@ -1,8 +1,9 @@ -use super::common::highlight; +use super::common; use super::{Chunk, Tag}; use anyhow::Result; use std::sync::LazyLock; use tree_sitter_highlight::HighlightConfiguration; +use tree_sitter_yaml::{HIGHLIGHTS_QUERY, LANGUAGE}; const NAMES: &[&str] = &[ "boolean", // YAML booleans @@ -21,46 +22,34 @@ const TAGS: &[Tag] = &[ Tag::Name, ]; -static YAML_CONFIG: LazyLock = LazyLock::new(|| { - let mut config = HighlightConfiguration::new( - tree_sitter_yaml::LANGUAGE.into(), - "", - tree_sitter_yaml::HIGHLIGHTS_QUERY, - "", - "", - ) - .expect("failed to build YAML syntax highlighter"); +static CONFIG: LazyLock = LazyLock::new(|| { + let mut config = HighlightConfiguration::new(LANGUAGE.into(), "", HIGHLIGHTS_QUERY, "", "") + .expect("failed to build syntax highlighter"); config.configure(NAMES); config }); -pub fn highlight_yaml(input: &[u8]) -> Result> { - highlight(&YAML_CONFIG, TAGS, input) +pub fn highlight(input: &[u8]) -> Result> { + common::highlight(&CONFIG, TAGS, input) } #[cfg(test)] mod tests { use super::*; - use crate::common; #[test] fn test_tags_ok() { - common::test_names_ok( - tree_sitter_yaml::LANGUAGE.into(), - tree_sitter_yaml::HIGHLIGHTS_QUERY, - NAMES, - TAGS, - ); + common::test_tags_ok(LANGUAGE.into(), HIGHLIGHTS_QUERY, NAMES, TAGS); } #[test] - fn test_highlight_yaml() { + fn test_highlight() { let input = b"\ string: \"value\"\n\ bool: true\n\ number: !fixed32 42 # comment\n\ "; - let chunks = highlight_yaml(input).unwrap(); + let chunks = highlight(input).unwrap(); assert_eq!( chunks, vec![ diff --git a/mitmproxy-rs/mitmproxy_rs/syntax_highlight.pyi b/mitmproxy-rs/mitmproxy_rs/syntax_highlight.pyi index 222fa648..be050888 100644 --- a/mitmproxy-rs/mitmproxy_rs/syntax_highlight.pyi +++ b/mitmproxy-rs/mitmproxy_rs/syntax_highlight.pyi @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Literal -def highlight(text: str, language: Literal["xml", "yaml", "error", "none"]) -> list[tuple[str, str]]: +def highlight(text: str, language: Literal["css", "javascript", "xml", "yaml", "none", "error"]) -> list[tuple[str, str]]: pass def languages() -> list[str]: