Files
Kigi-CLI/crates/codegen/kigi-markdown/src/syntax.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

200 lines
7.0 KiB
Rust

//! Syntax highlighting support using syntect.
use std::io::Cursor;
use std::path::Path;
use syntect::{
easy::HighlightLines,
highlighting::{Theme as SyntectTheme, ThemeSet},
parsing::{SyntaxReference, SyntaxSet},
};
/// Syntax highlighting configuration.
///
/// Loading the syntax set is expensive; create one instance and pass it to
/// the markdown renderer.
pub struct Syntect {
pub theme: SyntectTheme,
pub syntax_set: SyntaxSet,
}
impl Syntect {
/// `theme_bytes` must be a TextMate `.tmTheme` file. The syntax set is
/// two-face's extended one, covering the 250+ languages bat ships.
///
/// # Example
///
/// ```ignore
/// let syntect = Syntect::new(include_bytes!("assets/tokyo-night.tmTheme"));
/// ```
pub fn new(theme_bytes: &[u8]) -> Self {
let mut cursor = Cursor::new(theme_bytes);
let theme = ThemeSet::load_from_reader(&mut cursor).expect("Failed to load theme");
let syntax_set = two_face::syntax::extra_newlines();
Self { theme, syntax_set }
}
/// Resolves on the extension alone; the rest of the file name is ignored.
pub fn find_syntax_by_file_path(&self, file_path: &Path) -> Option<&SyntaxReference> {
let ext = file_path.extension()?.to_str()?;
self.syntax_set.find_syntax_by_extension(ext)
}
/// Find a syntax definition by language token (e.g., "rust", "python").
pub fn find_syntax_by_token(&self, token: &str) -> Option<&SyntaxReference> {
self.syntax_set.find_syntax_by_token(token)
}
pub fn highlight_lines_by_file_path(&self, file_path: &Path) -> Option<HighlightLines<'_>> {
Some(HighlightLines::new(
self.find_syntax_by_file_path(file_path)?,
&self.theme,
))
}
pub fn highlight_lines_for_token(&self, token: &str) -> Option<HighlightLines<'_>> {
Some(HighlightLines::new(
self.find_syntax_by_token(token)?,
&self.theme,
))
}
/// Highlighter for a fenced code block *info* string: a normal language token
/// (e.g. `rust`, `python`), or a **line-range citation** of the form
/// `lineStart:lineEnd:path/to/file.ext` where the syntax is resolved the same
/// way as [`Syntect::highlight_lines_by_file_path`] (see
/// [`Syntect::find_syntax_by_file_path`]).
///
/// If the string matches the citation form but no syntax is found for the
/// path, this falls back to [`Syntect::find_syntax_by_token`] with the full
/// `fence_info` string, so plain ` ```lang` blocks keep working and odd
/// citations degrade to a plain token lookup.
pub fn highlight_lines_for_fence_info(&self, fence_info: &str) -> Option<HighlightLines<'_>> {
Some(HighlightLines::new(
self.find_syntax_for_fence_info(fence_info)?,
&self.theme,
))
}
/// Resolve the [`SyntaxReference`] for a fenced code block *info* string,
/// using the SAME rules as [`Syntect::highlight_lines_for_fence_info`]:
/// a `lineStart:lineEnd:path` citation resolves by file path, otherwise
/// (or if the path has no known syntax) it falls back to a language token.
///
/// Exposed so the incremental open-code highlighter can build its own
/// resumable `ParseState`/`HighlightState` against exactly the syntax the
/// batch `HighlightLines` path would have used — keeping the two
/// byte-identical.
pub(crate) fn find_syntax_for_fence_info(&self, fence_info: &str) -> Option<&SyntaxReference> {
if let Some((_, _, path)) = parse_line_citation_fence_info(fence_info)
&& let Some(s) = self.find_syntax_by_file_path(Path::new(path))
{
return Some(s);
}
self.find_syntax_by_token(fence_info)
}
}
/// ```text
/// lineStart:lineEnd:path/to/file.ext
/// ```
///
/// The path is the segment after the **second** colon; it is then parsed with
/// [`Path::new`]. Paths with extra colons in the first two segments (e.g. some
/// Windows `C:...` forms) are not supported; use a repo-relative or
/// forward-slash form.
fn parse_line_citation_fence_info(info: &str) -> Option<(&str, &str, &str)> {
let mut it = info.splitn(3, ':');
let start = it.next()?;
let end = it.next()?;
let path = it.next()?;
if start.is_empty() || !start.chars().all(|c| c.is_ascii_digit()) {
return None;
}
if end.is_empty() || !end.chars().all(|c| c.is_ascii_digit()) {
return None;
}
if path.is_empty() {
return None;
}
Some((start, end, path))
}
/// Syntax highlight code, returning raw styled segments per line.
///
/// `fence_info` is the fenced code block *info* string (language tag or
/// `lineStart:lineEnd:path` citation form); see
/// [`Syntect::highlight_lines_for_fence_info`]. Lives here (not in `parse`)
/// so both the parser and the streaming highlighter caches depend one-way on
/// `syntax`.
pub(crate) fn syntax_highlight_raw(
syntect: Option<&Syntect>,
fence_info: &str,
text: &str,
) -> Option<Vec<Vec<(syntect::highlighting::Style, String)>>> {
use syntect::util::LinesWithEndings;
let syn = syntect?;
let mut hl = syn.highlight_lines_for_fence_info(fence_info)?;
let mut lines = Vec::new();
for line in LinesWithEndings::from(text) {
let highlighted = hl.highlight_line(line, &syn.syntax_set).ok()?;
lines.push(
highlighted
.into_iter()
.map(|(s, t)| (s, t.to_string()))
.collect(),
);
}
Some(lines)
}
/// Shared Syntect instance for tests, using the crate's bundled tokyo-night
/// theme.
#[cfg(any(test, fuzzing))]
#[allow(dead_code)]
pub fn test_syntect() -> &'static Syntect {
use std::sync::OnceLock;
static TEST_SYNTECT: OnceLock<Syntect> = OnceLock::new();
TEST_SYNTECT.get_or_init(|| Syntect::new(include_bytes!("../assets/tokyo-night.tmTheme")))
}
#[cfg(test)]
mod tests {
use super::parse_line_citation_fence_info;
#[test]
fn line_citation_fence_parses_start_end_path() {
assert_eq!(
parse_line_citation_fence_info("37:65:crates/example/src/tools/read.rs"),
Some(("37", "65", "crates/example/src/tools/read.rs"))
);
}
#[test]
fn line_citation_rejects_non_numeric_line() {
assert_eq!(parse_line_citation_fence_info("37:ab:file.rs"), None);
}
#[test]
fn line_citation_rejects_plain_lang_token() {
assert_eq!(parse_line_citation_fence_info("rust"), None);
assert_eq!(parse_line_citation_fence_info(""), None);
}
#[test]
fn highlight_lines_for_fence_info_resolves_citation_path_to_rust() {
let s = super::test_syntect();
assert!(
s.highlight_lines_for_fence_info("37:65:crates/codegen/kigi-markdown/src/parse.rs")
.is_some()
);
}
#[test]
fn highlight_lines_for_fence_info_still_accepts_rust_token() {
let s = super::test_syntect();
assert!(s.highlight_lines_for_fence_info("rust").is_some());
}
}