#![doc = include_str!("../README.md")]
use logos::Logos;
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Tag, TagEnd};
pub mod languages;
pub trait Highlight: Sized + for<'a> Logos<'a, Source = str> {
const LANG: &'static str;
const START: Self;
fn kind(tokens: &[Self; 2]) -> Kind;
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Kind {
None,
Glyph,
Literal,
Identifier,
SpecialIdentifier,
StrongIdentifier,
Keyword,
Comment,
}
static HIGHLIGHT_CLASS: [Option<&'static str>; 8] = {
let mut classes = [None; 8];
classes[Kind::Glyph as usize] = Some("glyph");
classes[Kind::Literal as usize] = Some("literal");
classes[Kind::Identifier as usize] = Some("identifier");
classes[Kind::SpecialIdentifier as usize] = Some("special-identifier");
classes[Kind::StrongIdentifier as usize] = Some("strong-identifier");
classes[Kind::Keyword as usize] = Some("keyword");
classes[Kind::Comment as usize] = Some("comment");
classes
};
#[derive(Debug, Default)]
pub struct SyntaxPreprocessor<'a, I: Iterator<Item = Event<'a>>> {
parent: I,
}
impl<'a, I: Iterator<Item = Event<'a>>> SyntaxPreprocessor<'a, I> {
pub fn new(parent: I) -> Self {
Self { parent }
}
}
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SyntaxPreprocessor<'a, I> {
type Item = Event<'a>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let lang = match self.parent.next()? {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) if !lang.is_empty() => lang,
#[cfg(feature = "latex2mathml")]
Event::InlineMath(c) => {
return Some(Event::Html(
latex2mathml::latex_to_mathml(
c.as_ref(),
latex2mathml::DisplayStyle::Inline,
)
.unwrap_or_else(|e| e.to_string())
.into(),
));
}
#[cfg(feature = "latex2mathml")]
Event::DisplayMath(c) => {
return Some(Event::Html(
latex2mathml::latex_to_mathml(
c.as_ref(),
latex2mathml::DisplayStyle::Block,
)
.unwrap_or_else(|e| e.to_string())
.into(),
));
}
other => return Some(other),
};
let next = self.parent.next();
let code = match next {
Some(Event::Text(c)) => {
let mut code = c;
loop {
match self.parent.next() {
Some(Event::Text(ref c)) => {
code = {
let mut s = code.into_string();
s.push_str(c);
CowStr::Boxed(s.into())
}
}
Some(Event::End(TagEnd::CodeBlock)) | None => break,
Some(e) => {
return Some(Event::Text(
format!("Unexpected markdown event {:#?}", e).into(),
))
}
}
}
code
}
Some(Event::End(TagEnd::CodeBlock)) | None => CowStr::Borrowed(""),
Some(e) => {
return Some(Event::Text(
format!("Unexpected markdown event {:#?}", e).into(),
))
}
};
let mut html = String::with_capacity(code.len() + code.len() / 4 + 60);
html.push_str("<pre><code class=\"language-");
html.push_str(lang.as_ref());
html.push_str("\">");
match lang.as_ref() {
"rust" | "rs" => highlight::<languages::Rust>(&code, &mut html),
"js" | "javascript" => highlight::<languages::JavaScript>(&code, &mut html),
"toml" => highlight::<languages::Toml>(&code, &mut html),
"sh" | "shell" | "bash" => highlight::<languages::Sh>(&code, &mut html),
_ => write_escaped(&mut html, &code),
}
html.push_str("</code></pre>");
Some(Event::Html(html.into()))
}
}
#[inline]
fn write_escaped(s: &mut String, part: &str) {
let mut start = 0;
for (idx, byte) in part.bytes().enumerate() {
let replace = match byte {
b'<' => "<",
b'>' => ">",
b'&' => "&",
b'"' => """,
_ => continue,
};
s.push_str(&part[start..idx]);
s.push_str(replace);
start = idx + 1;
}
s.push_str(&part[start..]);
}
#[inline]
pub fn highlight<'a, Token>(source: &'a str, buf: &mut String)
where
Token: Highlight + Eq + Copy,
<Token as Logos<'a>>::Extras: Default,
{
let mut lex = Token::lexer(source);
let mut open = Kind::None;
let mut last = 0usize;
let mut tokens = [Token::START; 2];
while let Some(token) = lex.next() {
if tokens[1] != Token::START {
tokens[0] = tokens[1];
}
tokens[1] = token.unwrap_or(Token::START);
let kind = Token::kind(&tokens);
if open != kind {
if open != Kind::None {
buf.push_str("</span>");
}
write_escaped(buf, &source[last..lex.span().start]);
if let Some(tag) = HIGHLIGHT_CLASS[kind as usize] {
buf.push_str("<span class=\"");
buf.push_str(tag);
buf.push_str("\">");
}
open = kind;
write_escaped(buf, lex.slice());
} else {
write_escaped(buf, &source[last..lex.span().end]);
}
last = lex.span().end;
}
if open != Kind::None {
buf.push_str("</span>");
}
}