use std::ops::Range;
use jotdown::{Container, Event, Parser};
use crate::injection::InjectionRegistry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SyntaxKind {
Heading,
Emphasis,
Strong,
Strikethrough,
Mark,
Verbatim,
Math,
CodeBlock,
RawBlock,
Link,
Image,
Blockquote,
Div,
Keyword,
StringLit,
Number,
Comment,
Function,
Type,
Punctuation,
Identifier,
Url,
Mention,
Tag,
Email,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
pub range: Range<usize>,
pub kind: SyntaxKind,
}
fn kind_of(container: &Container) -> Option<SyntaxKind> {
Some(match container {
Container::Heading { .. } => SyntaxKind::Heading,
Container::Emphasis => SyntaxKind::Emphasis,
Container::Strong => SyntaxKind::Strong,
Container::Delete => SyntaxKind::Strikethrough,
Container::Mark => SyntaxKind::Mark,
Container::Verbatim => SyntaxKind::Verbatim,
Container::Math { .. } => SyntaxKind::Math,
Container::CodeBlock { .. } => SyntaxKind::CodeBlock,
Container::RawBlock { .. } => SyntaxKind::RawBlock,
Container::RawInline { .. } => SyntaxKind::RawBlock,
Container::Link(..) => SyntaxKind::Link,
Container::Image(..) => SyntaxKind::Image,
Container::Blockquote => SyntaxKind::Blockquote,
Container::Div { .. } => SyntaxKind::Div,
_ => return None,
})
}
pub fn highlight_djot(src: &str) -> Vec<Span> {
let mut spans = Vec::new();
let mut stack: Vec<(SyntaxKind, usize)> = Vec::new();
for (event, range) in Parser::new(src).into_offset_iter() {
match event {
Event::Start(container, _attrs) => {
if let Some(kind) = kind_of(&container) {
stack.push((kind, range.start));
}
}
Event::End(container) => {
if kind_of(&container).is_some() {
if let Some((kind, start)) = stack.pop() {
spans.push(Span {
range: start..range.end,
kind,
});
}
}
}
_ => {}
}
}
spans
}
struct CodeCtx {
lang: String,
inner: Option<(usize, usize)>,
}
impl CodeCtx {
fn new(lang: &str) -> Self {
Self {
lang: lang.to_string(),
inner: None,
}
}
fn extend(&mut self, r: Range<usize>) {
self.inner = Some(match self.inner {
Some((s, e)) => (s.min(r.start), e.max(r.end)),
None => (r.start, r.end),
});
}
}
pub fn highlight(src: &str, registry: &InjectionRegistry) -> Vec<Span> {
let mut spans = Vec::new();
let mut stack: Vec<(SyntaxKind, usize)> = Vec::new();
let mut code: Option<CodeCtx> = None;
for (event, range) in Parser::new(src).into_offset_iter() {
match event {
Event::Start(container, _attrs) => {
match &container {
Container::CodeBlock { language } => {
code = Some(CodeCtx::new(language.as_ref()))
}
Container::RawBlock { format } => code = Some(CodeCtx::new(format.as_ref())),
_ => {}
}
if let Some(kind) = kind_of(&container) {
stack.push((kind, range.start));
}
}
Event::Str(_) => {
if let Some(ctx) = code.as_mut() {
ctx.extend(range.clone());
}
}
Event::End(container) => {
if matches!(
&container,
Container::CodeBlock { .. } | Container::RawBlock { .. }
) {
if let Some(ctx) = code.take() {
if !ctx.lang.is_empty() {
if let Some((s, e)) = ctx.inner {
if let Some(injected) = registry.lex_at(&ctx.lang, &src[s..e], s) {
spans.extend(injected);
}
}
}
}
}
if kind_of(&container).is_some() {
if let Some((kind, start)) = stack.pop() {
spans.push(Span {
range: start..range.end,
kind,
});
}
}
}
_ => {}
}
}
spans
}
#[cfg(test)]
mod tests {
use super::*;
fn slice_of<'a>(src: &'a str, spans: &[Span], kind: SyntaxKind) -> Option<&'a str> {
spans
.iter()
.find(|s| s.kind == kind)
.map(|s| &src[s.range.clone()])
}
#[test]
fn dump_spans() {
for src in [
"# A heading",
"Some _emphasis_ and *strong* and `code` here.",
"A [link](https://example.com) and .",
"```rust\nfn main() {}\n```",
"> a quote\n",
"::: note\nbody\n:::\n",
] {
eprintln!("--- {src:?}");
for s in highlight_djot(src) {
eprintln!(" {:?} {:?} = {:?}", s.kind, s.range.clone(), &src[s.range]);
}
}
}
#[test]
fn heading_is_colored() {
let src = "# Title";
let spans = highlight_djot(src);
assert!(
spans.iter().any(|s| s.kind == SyntaxKind::Heading),
"expected a Heading span, got {spans:?}"
);
}
#[test]
fn emphasis_and_strong_cover_their_delimiters() {
let src = "_em_ *st*";
let spans = highlight_djot(src);
assert_eq!(slice_of(src, &spans, SyntaxKind::Emphasis), Some("_em_"));
assert_eq!(slice_of(src, &spans, SyntaxKind::Strong), Some("*st*"));
}
#[test]
fn inline_code_is_verbatim() {
let src = "before `code` after";
let spans = highlight_djot(src);
assert_eq!(slice_of(src, &spans, SyntaxKind::Verbatim), Some("`code`"));
}
#[test]
fn fenced_code_block_is_one_region() {
let src = "```rust\nfn main() {}\n```";
let spans = highlight_djot(src);
let n = spans
.iter()
.filter(|s| s.kind == SyntaxKind::CodeBlock)
.count();
assert_eq!(n, 1, "expected one CodeBlock span, got {spans:?}");
}
#[test]
fn nested_spans_both_emit() {
let src = "_a *b* c_";
let spans = highlight_djot(src);
let em = spans
.iter()
.find(|s| s.kind == SyntaxKind::Emphasis)
.unwrap();
let st = spans.iter().find(|s| s.kind == SyntaxKind::Strong).unwrap();
assert!(
em.range.start <= st.range.start && st.range.end <= em.range.end,
"strong {:?} should nest inside emphasis {:?}",
st.range,
em.range
);
}
#[test]
fn plain_text_has_no_spans() {
assert!(highlight_djot("just plain text, nothing fancy").is_empty());
}
#[test]
fn code_block_injects_inner_language() {
let reg = crate::pack::default_pack();
let src = "```json\n{\"a\": 1}\n```";
let spans = highlight(src, ®);
assert!(spans.iter().any(|s| s.kind == SyntaxKind::CodeBlock));
let string_slices: Vec<_> = spans
.iter()
.filter(|s| s.kind == SyntaxKind::StringLit)
.map(|s| &src[s.range.clone()])
.collect();
assert!(
string_slices.contains(&"\"a\""),
"expected a JSON string token, got {spans:?}"
);
assert!(
spans
.iter()
.any(|s| s.kind == SyntaxKind::Number && &src[s.range.clone()] == "1"),
"expected a JSON number token, got {spans:?}"
);
}
#[test]
fn unregistered_code_language_stays_a_plain_region() {
let reg = InjectionRegistry::new(); let src = "```rust\nfn x() {}\n```";
let spans = highlight(src, ®);
assert!(spans.iter().any(|s| s.kind == SyntaxKind::CodeBlock));
assert!(
!spans.iter().any(|s| s.kind == SyntaxKind::Keyword),
"no injection without a registered lexer"
);
}
}