From 5570b2486a557ddc742e167e332a453d67d82d0c Mon Sep 17 00:00:00 2001 From: eas4ai Date: Mon, 19 Oct 2026 20:12:33 -0400 Subject: [PATCH 1/3] Add Rust syntax highlighting --- src/Highlighter.php | 2 + .../Rust/Patterns/RustAttributePattern.php | 24 ++++ .../Rust/Patterns/RustCharacterPattern.php | 24 ++++ .../Rust/Patterns/RustCommentPattern.php | 24 ++++ .../Rust/Patterns/RustFunctionPattern.php | 24 ++++ .../Rust/Patterns/RustKeywordPattern.php | 33 +++++ .../Rust/Patterns/RustLifetimePattern.php | 24 ++++ .../Rust/Patterns/RustLiteralPattern.php | 24 ++++ .../Rust/Patterns/RustMacroPattern.php | 24 ++++ .../Rust/Patterns/RustNumberPattern.php | 24 ++++ .../Rust/Patterns/RustOperatorPattern.php | 24 ++++ .../Rust/Patterns/RustPunctuationPattern.php | 24 ++++ .../Rust/Patterns/RustStringPattern.php | 24 ++++ .../Rust/Patterns/RustTypePattern.php | 24 ++++ src/Languages/Rust/RustLanguage.php | 56 +++++++++ tests/Bench/Fixtures/rust.txt | 20 +++ tests/Bench/HighlighterBench.php | 1 + tests/Languages/Rust/RustLanguageTest.php | 118 ++++++++++++++++++ 18 files changed, 518 insertions(+) create mode 100644 src/Languages/Rust/Patterns/RustAttributePattern.php create mode 100644 src/Languages/Rust/Patterns/RustCharacterPattern.php create mode 100644 src/Languages/Rust/Patterns/RustCommentPattern.php create mode 100644 src/Languages/Rust/Patterns/RustFunctionPattern.php create mode 100644 src/Languages/Rust/Patterns/RustKeywordPattern.php create mode 100644 src/Languages/Rust/Patterns/RustLifetimePattern.php create mode 100644 src/Languages/Rust/Patterns/RustLiteralPattern.php create mode 100644 src/Languages/Rust/Patterns/RustMacroPattern.php create mode 100644 src/Languages/Rust/Patterns/RustNumberPattern.php create mode 100644 src/Languages/Rust/Patterns/RustOperatorPattern.php create mode 100644 src/Languages/Rust/Patterns/RustPunctuationPattern.php create mode 100644 src/Languages/Rust/Patterns/RustStringPattern.php create mode 100644 src/Languages/Rust/Patterns/RustTypePattern.php create mode 100644 src/Languages/Rust/RustLanguage.php create mode 100644 tests/Bench/Fixtures/rust.txt create mode 100644 tests/Languages/Rust/RustLanguageTest.php diff --git a/src/Highlighter.php b/src/Highlighter.php index 843cf5b6..b86a147d 100644 --- a/src/Highlighter.php +++ b/src/Highlighter.php @@ -28,6 +28,7 @@ use Tempest\Highlight\Languages\Php\PhpLanguage; use Tempest\Highlight\Languages\Python\PythonLanguage; use Tempest\Highlight\Languages\Ruby\RubyLanguage; +use Tempest\Highlight\Languages\Rust\RustLanguage; use Tempest\Highlight\Languages\Scss\ScssLanguage; use Tempest\Highlight\Languages\Sema\SemaLanguage; use Tempest\Highlight\Languages\Sql\SqlLanguage; @@ -87,6 +88,7 @@ public function __construct( ->addLanguage(new PhpLanguage()) ->addLanguage(new PythonLanguage()) ->addLanguage(new RubyLanguage()) + ->addLanguage(new RustLanguage()) ->addLanguage(new ScssLanguage()) ->addLanguage(new SemaLanguage()) ->addLanguage(new SqlLanguage()) diff --git a/src/Languages/Rust/Patterns/RustAttributePattern.php b/src/Languages/Rust/Patterns/RustAttributePattern.php new file mode 100644 index 00000000..cd4ca387 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustAttributePattern.php @@ -0,0 +1,24 @@ +#!?\[\s*+(?:r#)?[A-Za-z_][\w:]*)'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::ATTRIBUTE; + } +} diff --git a/src/Languages/Rust/Patterns/RustCharacterPattern.php b/src/Languages/Rust/Patterns/RustCharacterPattern.php new file mode 100644 index 00000000..f3616c0f --- /dev/null +++ b/src/Languages/Rust/Patterns/RustCharacterPattern.php @@ -0,0 +1,24 @@ +b?'(?:\\\\.|[^'\\\\])')"; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::VALUE; + } +} diff --git a/src/Languages/Rust/Patterns/RustCommentPattern.php b/src/Languages/Rust/Patterns/RustCommentPattern.php new file mode 100644 index 00000000..244613e6 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustCommentPattern.php @@ -0,0 +1,24 @@ +\/\/[^\r\n]*|(?&block))(?(DEFINE)(?\/\*(?:[^*\/]++|\/(?!\*)|\*(?!\/)|(?&block))*\*\/))/s'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::COMMENT; + } +} diff --git a/src/Languages/Rust/Patterns/RustFunctionPattern.php b/src/Languages/Rust/Patterns/RustFunctionPattern.php new file mode 100644 index 00000000..4982d3f8 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustFunctionPattern.php @@ -0,0 +1,24 @@ +(?:r#)?[A-Za-z_]\w*)(?=\s*+(?:<[^<>{};()]*>\s*+)?\()'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::PROPERTY; + } +} diff --git a/src/Languages/Rust/Patterns/RustKeywordPattern.php b/src/Languages/Rust/Patterns/RustKeywordPattern.php new file mode 100644 index 00000000..e58d6e92 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustKeywordPattern.php @@ -0,0 +1,33 @@ +' . implode('|', self::KEYWORDS) . ')\b'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::KEYWORD; + } +} diff --git a/src/Languages/Rust/Patterns/RustLifetimePattern.php b/src/Languages/Rust/Patterns/RustLifetimePattern.php new file mode 100644 index 00000000..02ac8e87 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustLifetimePattern.php @@ -0,0 +1,24 @@ +'[A-Za-z_]\\w*)(?!')\\b"; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::TYPE; + } +} diff --git a/src/Languages/Rust/Patterns/RustLiteralPattern.php b/src/Languages/Rust/Patterns/RustLiteralPattern.php new file mode 100644 index 00000000..04c37255 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustLiteralPattern.php @@ -0,0 +1,24 @@ +true|false)\b'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::LITERAL; + } +} diff --git a/src/Languages/Rust/Patterns/RustMacroPattern.php b/src/Languages/Rust/Patterns/RustMacroPattern.php new file mode 100644 index 00000000..7561377d --- /dev/null +++ b/src/Languages/Rust/Patterns/RustMacroPattern.php @@ -0,0 +1,24 @@ +(?:r#)?[A-Za-z_]\w*!)'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::PROPERTY; + } +} diff --git a/src/Languages/Rust/Patterns/RustNumberPattern.php b/src/Languages/Rust/Patterns/RustNumberPattern.php new file mode 100644 index 00000000..3618035a --- /dev/null +++ b/src/Languages/Rust/Patterns/RustNumberPattern.php @@ -0,0 +1,24 @@ +(?:0x[\dA-Fa-f_]+|0o[0-7_]+|0b[01_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?[\d_]+)?)(?:f32|f64|i(?:8|16|32|64|128|size)|u(?:8|16|32|64|128|size))?)\b'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::NUMBER; + } +} diff --git a/src/Languages/Rust/Patterns/RustOperatorPattern.php b/src/Languages/Rust/Patterns/RustOperatorPattern.php new file mode 100644 index 00000000..6166e090 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustOperatorPattern.php @@ -0,0 +1,24 @@ +::|<<=?|>>=?|->|=>|\.\.[=.]?|&&|\|\||==|!=|<=|>=|[-+*\/%&|^!]=?|=(?!=|>)|[<>?.:@])'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::OPERATOR; + } +} diff --git a/src/Languages/Rust/Patterns/RustPunctuationPattern.php b/src/Languages/Rust/Patterns/RustPunctuationPattern.php new file mode 100644 index 00000000..d7c51535 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustPunctuationPattern.php @@ -0,0 +1,24 @@ +[{}()\[\],;])'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::OPERATOR; + } +} diff --git a/src/Languages/Rust/Patterns/RustStringPattern.php b/src/Languages/Rust/Patterns/RustStringPattern.php new file mode 100644 index 00000000..2884ac95 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustStringPattern.php @@ -0,0 +1,24 @@ +b?r(?#{0,255})".*?"\k|b?"(?:\\\\.|[^"\\\\])*")/s'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::VALUE; + } +} diff --git a/src/Languages/Rust/Patterns/RustTypePattern.php b/src/Languages/Rust/Patterns/RustTypePattern.php new file mode 100644 index 00000000..58772387 --- /dev/null +++ b/src/Languages/Rust/Patterns/RustTypePattern.php @@ -0,0 +1,24 @@ +bool|char|str|f32|f64|i(?:8|16|32|64|128|size)|u(?:8|16|32|64|128|size)|[A-Z][\w]*)(?![\w!])'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::TYPE; + } +} diff --git a/src/Languages/Rust/RustLanguage.php b/src/Languages/Rust/RustLanguage.php new file mode 100644 index 00000000..da33957e --- /dev/null +++ b/src/Languages/Rust/RustLanguage.php @@ -0,0 +1,56 @@ + { + label: &'a str, + value: T, +} + +impl<'a, T: Display> Point<'a, T> { + fn render(&self) -> String { + format!(r#"{}: {}"#, self.label, self.value) + } +} + +fn main() { + let point = Point { label: "answer", value: 42_u8 }; + println!("{}", point.render()); +} diff --git a/tests/Bench/HighlighterBench.php b/tests/Bench/HighlighterBench.php index 154b03c3..fb6d3a68 100644 --- a/tests/Bench/HighlighterBench.php +++ b/tests/Bench/HighlighterBench.php @@ -37,6 +37,7 @@ final class HighlighterBench 'php' => 'php.txt', 'python' => 'python.txt', 'ruby' => 'ruby.txt', + 'rust' => 'rust.txt', 'scss' => 'scss.txt', 'sema' => 'sema.txt', 'sql' => 'sql.txt', diff --git a/tests/Languages/Rust/RustLanguageTest.php b/tests/Languages/Rust/RustLanguageTest.php new file mode 100644 index 00000000..93591ec7 --- /dev/null +++ b/tests/Languages/Rust/RustLanguageTest.php @@ -0,0 +1,118 @@ +assertContains('rust', $highlighter->getSupportedLanguageNames()); + $this->assertContains('rs', $highlighter->getSupportedLanguageNames()); + + foreach (['rust', 'rs'] as $name) { + $highlighter->parse('', $name); + + $this->assertInstanceOf(RustLanguage::class, $highlighter->getCurrentLanguage()); + } + } + + #[Test] + #[DataProvider('provide_it_highlights_representative_syntax_cases')] + public function it_highlights_representative_syntax(string $source, string $expected): void + { + $highlighted = (new Highlighter())->parse($source, 'rust'); + + $this->assertStringContainsString($expected, $highlighted); + } + + public static function provide_it_highlights_representative_syntax_cases(): iterable + { + yield 'line comment' => ['// note', '// note']; + yield 'nested block comment' => ['/* outer /* inner */ outer */', '/* outer /* inner */ outer */']; + yield 'attribute' => ['#[inline]', '#']; + yield 'string' => ['"hello"', '"hello"']; + yield 'raw string' => ['r#"hello"#', 'r#"hello"#']; + yield 'character' => ["'x'", '\'x\'']; + yield 'lifetime' => ["'a", '\'a']; + yield 'number' => ['42_u8', '42_u8']; + yield 'keyword' => ['async', 'async']; + yield 'self keyword' => ['self', 'self']; + yield 'Self keyword' => ['Self', 'Self']; + yield 'super keyword' => ['super', 'super']; + yield 'type' => ['String', 'String']; + yield 'macro' => ['println!', 'println!']; + yield 'function' => ['run()', 'run']; + yield 'operator' => ['->', '->']; + yield 'punctuation' => [';', ';']; + } + + #[Test] + public function it_escapes_html_and_strips_reserved_markers(): void + { + $source = "fn render<'a, T>(value: &'a T) -> &'a str {\r\n" + . " println!(\"<©> ❷img src=x onerror=alert(1)❸ ❶❹❿\");\r" + . " \"\"\r\n" + . '}'; + + $highlighted = (new Highlighter())->parse($source, 'rust'); + $renderedSource = html_entity_decode(strip_tags($highlighted), ENT_QUOTES | ENT_HTML5); + + $expectedSource = str_replace(["\r\n", "\r"], "\n", str_replace(['❶', '❷', '❸', '❹', '❿'], '', $source)); + + $this->assertSame($expectedSource, $renderedSource); + $this->assertStringNotContainsString('