mod lang;
mod scan;
use std::ops::Range;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Kind {
Plain,
Keyword,
Type,
Function,
String,
Comment,
Number,
Constant,
Punctuation,
Operator,
Attribute,
Property,
Variable,
Tag,
Heading,
Emphasis,
Link,
Inserted,
Deleted,
Meta,
}
impl Kind {
pub const ALL: [Kind; 20] = [
Kind::Plain,
Kind::Keyword,
Kind::Type,
Kind::Function,
Kind::String,
Kind::Comment,
Kind::Number,
Kind::Constant,
Kind::Punctuation,
Kind::Operator,
Kind::Attribute,
Kind::Property,
Kind::Variable,
Kind::Tag,
Kind::Heading,
Kind::Emphasis,
Kind::Link,
Kind::Inserted,
Kind::Deleted,
Kind::Meta,
];
pub fn name(self) -> &'static str {
match self {
Kind::Plain => "plain",
Kind::Keyword => "keyword",
Kind::Type => "type",
Kind::Function => "function",
Kind::String => "string",
Kind::Comment => "comment",
Kind::Number => "number",
Kind::Constant => "constant",
Kind::Punctuation => "punctuation",
Kind::Operator => "operator",
Kind::Attribute => "attribute",
Kind::Property => "property",
Kind::Variable => "variable",
Kind::Tag => "tag",
Kind::Heading => "heading",
Kind::Emphasis => "emphasis",
Kind::Link => "link",
Kind::Inserted => "inserted",
Kind::Deleted => "deleted",
Kind::Meta => "meta",
}
}
}
pub type Span = (Range<usize>, Kind);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct State {
pub(crate) mode: u8,
pub(crate) n: u16,
pub(crate) h: u32,
}
impl State {
pub const START: State = State { mode: 0, n: 0, h: 0 };
pub fn to_bits(self) -> u64 {
u64::from(self.mode) | (u64::from(self.n) << 8) | (u64::from(self.h) << 32)
}
pub fn from_bits(bits: u64) -> State {
State { mode: (bits & 0xff) as u8, n: ((bits >> 8) & 0xffff) as u16, h: (bits >> 32) as u32 }
}
pub(crate) fn pack24(self) -> u32 {
u32::from(self.mode) | (u32::from(self.n) << 8)
}
pub(crate) fn unpack24(v: u32) -> State {
State { mode: (v & 0xff) as u8, n: ((v >> 8) & 0xffff) as u16, h: 0 }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Lang {
Rust,
TypeScript,
JavaScript,
Go,
C,
Cpp,
Java,
CSharp,
Python,
Shell,
Sql,
Json,
Toml,
Yaml,
Html,
Css,
Markdown,
Diff,
}
const ALIASES: &[(&str, Lang)] = &[
("rust", Lang::Rust),
("rs", Lang::Rust),
("typescript", Lang::TypeScript),
("ts", Lang::TypeScript),
("tsx", Lang::TypeScript),
("mts", Lang::TypeScript),
("cts", Lang::TypeScript),
("javascript", Lang::JavaScript),
("js", Lang::JavaScript),
("jsx", Lang::JavaScript),
("mjs", Lang::JavaScript),
("cjs", Lang::JavaScript),
("go", Lang::Go),
("golang", Lang::Go),
("c", Lang::C),
("h", Lang::C),
("cpp", Lang::Cpp),
("c++", Lang::Cpp),
("cc", Lang::Cpp),
("cxx", Lang::Cpp),
("hpp", Lang::Cpp),
("hh", Lang::Cpp),
("hxx", Lang::Cpp),
("java", Lang::Java),
("csharp", Lang::CSharp),
("c#", Lang::CSharp),
("cs", Lang::CSharp),
("python", Lang::Python),
("py", Lang::Python),
("pyi", Lang::Python),
("shell", Lang::Shell),
("sh", Lang::Shell),
("bash", Lang::Shell),
("zsh", Lang::Shell),
("console", Lang::Shell),
("sql", Lang::Sql),
("json", Lang::Json),
("jsonc", Lang::Json),
("json5", Lang::Json),
("toml", Lang::Toml),
("yaml", Lang::Yaml),
("yml", Lang::Yaml),
("html", Lang::Html),
("htm", Lang::Html),
("xml", Lang::Html),
("svg", Lang::Html),
("xhtml", Lang::Html),
("vue", Lang::Html),
("css", Lang::Css),
("scss", Lang::Css),
("less", Lang::Css),
("markdown", Lang::Markdown),
("md", Lang::Markdown),
("mdx", Lang::Markdown),
("diff", Lang::Diff),
("patch", Lang::Diff),
];
const FILE_NAMES: &[(&str, Lang)] = &[
(".bashrc", Lang::Shell),
(".zshrc", Lang::Shell),
(".profile", Lang::Shell),
(".bash_profile", Lang::Shell),
("Cargo.lock", Lang::Toml),
("PKGBUILD", Lang::Shell),
(".gitconfig", Lang::Toml),
];
impl Lang {
pub const ALL: [Lang; 18] = [
Lang::Rust,
Lang::TypeScript,
Lang::JavaScript,
Lang::Go,
Lang::C,
Lang::Cpp,
Lang::Java,
Lang::CSharp,
Lang::Python,
Lang::Shell,
Lang::Sql,
Lang::Json,
Lang::Toml,
Lang::Yaml,
Lang::Html,
Lang::Css,
Lang::Markdown,
Lang::Diff,
];
pub fn name(self) -> &'static str {
match self {
Lang::Rust => "rust",
Lang::TypeScript => "typescript",
Lang::JavaScript => "javascript",
Lang::Go => "go",
Lang::C => "c",
Lang::Cpp => "cpp",
Lang::Java => "java",
Lang::CSharp => "csharp",
Lang::Python => "python",
Lang::Shell => "shell",
Lang::Sql => "sql",
Lang::Json => "json",
Lang::Toml => "toml",
Lang::Yaml => "yaml",
Lang::Html => "html",
Lang::Css => "css",
Lang::Markdown => "markdown",
Lang::Diff => "diff",
}
}
pub fn from_name(name: &str) -> Option<Lang> {
let name = name.trim();
ALIASES.iter().find(|(alias, _)| alias.eq_ignore_ascii_case(name)).map(|(_, lang)| *lang)
}
pub fn for_path(path: &str) -> Option<Lang> {
let file = path.rsplit(['/', '\\']).next().unwrap_or(path);
if let Some((_, lang)) = FILE_NAMES.iter().find(|(name, _)| *name == file) {
return Some(*lang);
}
let (stem, ext) = file.rsplit_once('.')?;
if stem.is_empty() {
return None;
}
Lang::from_name(ext)
}
}
pub fn lang_for_path(path: &str) -> Option<&'static str> {
Lang::for_path(path).map(Lang::name)
}
pub fn highlight(lang: Option<&str>, text: &str) -> Vec<Span> {
let mut out = Vec::new();
highlight_into(lang.and_then(Lang::from_name), text, &mut out);
out
}
pub fn highlight_into(lang: Option<Lang>, text: &str, out: &mut Vec<Span>) {
if text.is_empty() {
return;
}
let Some(lang) = lang else {
out.push((0..text.len(), Kind::Plain));
return;
};
let mut state = State::START;
let mut base = 0usize;
for line in text.split_inclusive('\n') {
state = line_into(lang, line, state, base, out);
base = base.saturating_add(line.len());
}
}
pub fn highlight_line(lang: Lang, line: &str, state: State, out: &mut Vec<Span>) -> State {
line_into(lang, line, state, 0, out)
}
fn line_into(lang: Lang, line: &str, state: State, base: usize, out: &mut Vec<Span>) -> State {
let body = line.strip_suffix('\n').unwrap_or(line);
let body = body.strip_suffix('\r').unwrap_or(body);
let mut o = scan::Out::new(out, line, base);
let next = lang::lex(lang, body, state, &mut o);
o.finish();
next
}
pub trait Highlighter {
fn supports(&self, lang: &str) -> bool;
fn highlight_line(&self, lang: &str, line: &str, state: State, out: &mut Vec<Span>) -> State;
fn highlight(&self, lang: &str, text: &str, out: &mut Vec<Span>) {
let mut state = State::START;
let mut base = 0usize;
let mut line_spans = Vec::new();
for line in text.split_inclusive('\n') {
line_spans.clear();
state = self.highlight_line(lang, line, state, &mut line_spans);
for (range, kind) in line_spans.drain(..) {
let range = range.start.saturating_add(base)..range.end.saturating_add(base);
match out.last_mut() {
Some((last, k)) if *k == kind && last.end == range.start => last.end = range.end,
_ => out.push((range, kind)),
}
}
base = base.saturating_add(line.len());
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Builtin;
impl Highlighter for Builtin {
fn supports(&self, lang: &str) -> bool {
Lang::from_name(lang).is_some()
}
fn highlight_line(&self, lang: &str, line: &str, state: State, out: &mut Vec<Span>) -> State {
match Lang::from_name(lang) {
Some(l) => highlight_line(l, line, state, out),
None => {
if !line.is_empty() {
out.push((0..line.len(), Kind::Plain));
}
state
}
}
}
fn highlight(&self, lang: &str, text: &str, out: &mut Vec<Span>) {
highlight_into(Lang::from_name(lang), text, out);
}
}