From 24b2b314e0d5661de6e9b596e33ffe923bdae8fa Mon Sep 17 00:00:00 2001 From: MaksZhukov Date: Wed, 5 Aug 2026 13:19:48 +0200 Subject: [PATCH 1/2] fix: reuse Rust string rules inside attributes (closes #3817) The `meta` mode for Rust attributes declared its own minimal string rule, so only plain `"..."` literals were recognised inside `#[...]`. Raw strings and byte strings were mis-highlighted: in `#[doc = r#"a "quoted" word"#]` the literal was cut at the first inner quote, leaving the rest of the attribute wrongly scoped. Attributes hold arbitrary token trees, so any string literal that is valid elsewhere is valid there too. Extract the existing string modes into named constants and reuse them in `meta` instead of duplicating a weaker version. The character-literal mode is deliberately left out of `meta` so that lifetimes in attribute token trees (`#[foo(Bar<'a>)]`) are not treated as an unterminated char literal. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGES.md | 2 ++ src/languages/rust.js | 54 ++++++++++++++--------------- test/markup/rust/strings.expect.txt | 5 +++ test/markup/rust/strings.txt | 5 +++ 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7050702f5f..7ad1476eb4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ Core Grammars: - enh(dos) add `batch` as an alias, issue #4395 [Hashim Khan][] - fix(lisp) preserve highlighting after quoted multiplication expressions [arturict][] - fix(rust) recognize `\\` and `\"` char-literal escapes so highlighting doesn't leak, issue #4351 [Sarath Francis][] +- fix(rust) reuse the string rules inside attributes so raw and byte strings are highlighted there, issue #3817 [Maksim Zhukau][] - fix(cmake) only highlight standalone numbers, not digits that begin an identifier (e.g. `3rdparty`), issue #4170 [MarkXian][] - fix(cpp) require a word boundary before numeric literals so digits inside identifiers aren't highlighted as numbers, issue #4231 [Mark Xian][] - fix(c) only match real `atomic_*` type names, not C11 atomic functions, issue #3837 [MarkXian][] @@ -33,6 +34,7 @@ CONTRIBUTORS [Nicolas Le Cam]: https://github.com/KuSh [Konstantin Baltsat]: https://github.com/Baltsat [David Pavlovschii]: https://github.com/davidpavlovschi +[Maksim Zhukau]: https://github.com/MaksZhukov ## Version 11.11.3 diff --git a/src/languages/rust.js b/src/languages/rust.js index 7bfafc7ca5..eb481a2edb 100644 --- a/src/languages/rust.js +++ b/src/languages/rust.js @@ -157,6 +157,25 @@ export default function(hljs) { "assert_ne!", "debug_assert_ne!" ]; + const QUOTE_STRING = hljs.inherit(hljs.QUOTE_STRING_MODE, { + begin: /b?"/, + illegal: null + }); + const RAW_STRING = { + scope: 'string', + begin: /b?r(#*)"(.|\n)*?"\1(?!#)/ + }; + const CHARACTER = { + scope: 'string', + begin: /b?'/, + end: /'/, + contains: [ + { + scope: "char.escape", + match: /\\('|"|\\|\w|x\w{2}|u\w{4}|U\w{8})/ + } + ] + }; const TYPES = [ "i8", "i16", @@ -197,31 +216,14 @@ export default function(hljs) { contains: [ hljs.C_LINE_COMMENT_MODE, hljs.COMMENT('/\\*', '\\*/', { contains: [ 'self' ] }), - hljs.inherit(hljs.QUOTE_STRING_MODE, { - begin: /b?"/, - illegal: null - }), + QUOTE_STRING, { scope: 'symbol', // negative lookahead to avoid matching `'` begin: /'[a-zA-Z_][a-zA-Z0-9_]*(?!')/ }, - { - scope: 'string', - variants: [ - { begin: /b?r(#*)"(.|\n)*?"\1(?!#)/ }, - { - begin: /b?'/, - end: /'/, - contains: [ - { - scope: "char.escape", - match: /\\('|"|\\|\w|x\w{2}|u\w{4}|U\w{8})/ - } - ] - } - ] - }, + RAW_STRING, + CHARACTER, { scope: 'number', variants: [ @@ -260,14 +262,10 @@ export default function(hljs) { begin: '#!?\\[', end: '\\]', contains: [ - { - scope: 'string', - begin: /"/, - end: /"/, - contains: [ - hljs.BACKSLASH_ESCAPE - ] - } + // attributes hold arbitrary token trees, so any string literal + // that is valid elsewhere is valid here too + QUOTE_STRING, + RAW_STRING ] }, { diff --git a/test/markup/rust/strings.expect.txt b/test/markup/rust/strings.expect.txt index 6ddd0da66e..c0f1662215 100644 --- a/test/markup/rust/strings.expect.txt +++ b/test/markup/rust/strings.expect.txt @@ -24,3 +24,8 @@ br##"foo"##; #[error("\" appears in a string")] +#[doc = r#"a "quoted" word"#] +#[doc = b"bytes"] +#[doc = r##"multi +line "# raw"##] +#[foo(Bar<'a>)] diff --git a/test/markup/rust/strings.txt b/test/markup/rust/strings.txt index e4d85db470..a23d4585b8 100644 --- a/test/markup/rust/strings.txt +++ b/test/markup/rust/strings.txt @@ -24,3 +24,8 @@ br#"hello"#; br##"foo"##; #[error("\" appears in a string")] +#[doc = r#"a "quoted" word"#] +#[doc = b"bytes"] +#[doc = r##"multi +line "# raw"##] +#[foo(Bar<'a>)] From 3fb825c65b495945bd3be5070692c9c83bdea377 Mon Sep 17 00:00:00 2001 From: Maksim Zhukau Date: Mon, 10 Aug 2026 14:16:12 +0200 Subject: [PATCH 2/2] fix(rust) cap raw string delimiters at 255 hashes rustc rejects raw string literals delimited by more than 255 # symbols, so tighten the rule from #* to #{0,255} and add a many-hash spec sample. Co-Authored-By: Claude Fable 5 --- src/languages/rust.js | 3 ++- test/markup/rust/strings.expect.txt | 1 + test/markup/rust/strings.txt | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/languages/rust.js b/src/languages/rust.js index eb481a2edb..f0b9b5ae68 100644 --- a/src/languages/rust.js +++ b/src/languages/rust.js @@ -163,7 +163,8 @@ export default function(hljs) { }); const RAW_STRING = { scope: 'string', - begin: /b?r(#*)"(.|\n)*?"\1(?!#)/ + // rustc caps raw string delimiters at 255 hashes + begin: /b?r(#{0,255})"(.|\n)*?"\1(?!#)/ }; const CHARACTER = { scope: 'string', diff --git a/test/markup/rust/strings.expect.txt b/test/markup/rust/strings.expect.txt index c0f1662215..26f1188722 100644 --- a/test/markup/rust/strings.expect.txt +++ b/test/markup/rust/strings.expect.txt @@ -22,6 +22,7 @@ br" "; br#"hello"#; br##"foo"##; +r################"many hashes"################; #[error("\" appears in a string")] #[doc = r#"a "quoted" word"#] diff --git a/test/markup/rust/strings.txt b/test/markup/rust/strings.txt index a23d4585b8..502deac7de 100644 --- a/test/markup/rust/strings.txt +++ b/test/markup/rust/strings.txt @@ -22,6 +22,7 @@ r##" "### br" "; br#"hello"#; br##"foo"##; +r################"many hashes"################; #[error("\" appears in a string")] #[doc = r#"a "quoted" word"#]