diff --git a/src/html/djot.rs b/src/html/djot.rs
index a8e75fb..d473d76 100644
--- a/src/html/djot.rs
+++ b/src/html/djot.rs
@@ -2,6 +2,7 @@
//! Made concrete to avoid generic hell, with added treehouse-specific features.
use std::fmt::Write;
+use std::mem;
use std::ops::Range;
use codespan_reporting::diagnostic::Diagnostic;
@@ -42,6 +43,7 @@ impl Renderer<'_> {
renderer: self,
raw: Raw::None,
code_block: None,
+ code_block_text: String::new(),
img_alt_text: 0,
list_tightness: vec![],
not_first_line: false,
@@ -88,6 +90,7 @@ struct Writer<'a> {
raw: Raw,
code_block: Option>,
+ code_block_text: String,
img_alt_text: usize,
list_tightness: Vec,
not_first_line: bool,
@@ -445,6 +448,17 @@ impl<'a> Writer<'a> {
Container::CodeBlock { language } => {
let code_block = self.code_block.take().unwrap();
+ let rendered =
+ if let Some(syntax) = self.renderer.config.syntaxes.get(*language) {
+ let mut rendered = String::new();
+ highlight(&mut rendered, syntax, &self.code_block_text);
+ self.code_block_text.clear();
+ rendered
+ } else {
+ mem::take(&mut self.code_block_text)
+ };
+
+ out.push_str(&rendered);
out.push_str(match &code_block.kind {
CodeBlockKind::PlainText | CodeBlockKind::SyntaxHighlight => {
""
@@ -507,11 +521,8 @@ impl<'a> Writer<'a> {
Event::Str(s) => match self.raw {
Raw::None if self.img_alt_text > 0 => write_attr(s, out),
Raw::None => {
- let syntax = self.code_block.as_ref().and_then(|code_block| {
- self.renderer.config.syntaxes.get(code_block.language)
- });
- if let Some(syntax) = syntax {
- highlight(out, syntax, s);
+ if self.code_block.is_some() {
+ self.code_block_text.push_str(s);
} else {
write_text(s, out);
}
diff --git a/src/html/highlight.rs b/src/html/highlight.rs
index a54af25..76a5aca 100644
--- a/src/html/highlight.rs
+++ b/src/html/highlight.rs
@@ -15,6 +15,8 @@ use std::{collections::HashMap, fmt::Write};
use serde::{Deserialize, Serialize};
+use crate::html::highlight::tokenize::Token;
+
use self::compiled::CompiledSyntax;
use super::EscapeHtml;
@@ -82,13 +84,59 @@ pub struct Keyword {
pub only_replaces: Option,
}
-pub fn highlight(out: &mut String, syntax: &CompiledSyntax, code: &str) {
- let tokens = syntax.tokenize(code);
+fn write_tokens(
+ out: &mut String,
+ syntax: &CompiledSyntax,
+ code: &str,
+ tokens: impl Iterator- ,
+) {
for token in tokens {
+ let str = &code[token.range.clone()];
out.push_str("");
- _ = write!(out, "{}", EscapeHtml(&code[token.range]));
+ _ = write!(out, "{}", EscapeHtml(str));
out.push_str("");
}
}
+
+pub fn highlight(out: &mut String, syntax: &CompiledSyntax, code: &str) {
+ let tokens = syntax.tokenize(code);
+ let mut line = vec![];
+
+ let mut in_columns = false;
+ for token in tokens {
+ let str = &code[token.range.clone()];
+ line.push(token);
+
+ if str.ends_with('\n') {
+ let line_comment = if line.last().is_some_and(|token| {
+ Some(token.id) == syntax.comment_token_id
+ && code[token.range.clone()].ends_with('\n')
+ }) {
+ line.pop()
+ } else {
+ None
+ };
+
+ if let Some(line_comment) = line_comment {
+ if !in_columns {
+ out.push_str("");
+ in_columns = true;
+ }
+
+ out.push_str("");
+ write_tokens(out, syntax, code, line.drain(..));
+ out.push_str("");
+ write_tokens(out, syntax, code, [line_comment].into_iter());
+ } else {
+ if in_columns {
+ out.push_str("");
+ in_columns = false;
+ }
+
+ write_tokens(out, syntax, code, line.drain(..));
+ }
+ }
+ }
+}
diff --git a/src/html/highlight/compiled.rs b/src/html/highlight/compiled.rs
index 6ad669a..4c1d89b 100644
--- a/src/html/highlight/compiled.rs
+++ b/src/html/highlight/compiled.rs
@@ -17,6 +17,9 @@ pub struct CompiledSyntax {
pub patterns: Vec,
pub keywords: HashMap,
+
+ /// If there is a token named "comment", this is its ID.
+ pub comment_token_id: Option,
}
#[derive(Debug, Clone)]
@@ -111,9 +114,12 @@ pub fn compile_syntax(syntax: &Syntax) -> CompiledSyntax {
})
.collect();
+ let comment_token_id = token_names.iter().position(|name| name == "comment");
+
CompiledSyntax {
token_names,
patterns,
keywords,
+ comment_token_id,
}
}
diff --git a/static/css/doc.css b/static/css/doc.css
index c6bdcbf..74babc2 100644
--- a/static/css/doc.css
+++ b/static/css/doc.css
@@ -75,7 +75,7 @@ main.doc {
--recursive-wght: 500;
--recursive-mono: 0.5; /* You didn't expect a proportional font being used for code, did you. */
font-size: 95%;
- tab-size: 3;
+ tab-size: 3ch;
}
&.monospaced code {
@@ -160,7 +160,7 @@ main.doc {
& code {
--recursive-wght: 520;
font-size: 90%;
- tab-size: 2;
+ tab-size: 2ch;
}
}
}
diff --git a/static/css/main.css b/static/css/main.css
index a373272..3c40a58 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -1198,6 +1198,11 @@ th-literate-program[data-mode="output"] {
}
}
+.th-syntax-highlighting th-comment-columns {
+ display: grid;
+ grid-template-columns: repeat(2, max-content);
+}
+
.th-syntax-highlighting {
& .export {
text-decoration: underline dotted;
diff --git a/static/syntax/rust.json b/static/syntax/rust.json
index 2e4d258..6c353ea 100644
--- a/static/syntax/rust.json
+++ b/static/syntax/rust.json
@@ -1,6 +1,6 @@
{
"patterns": [
- { "regex": "\\/\\/.*", "is": "comment" },
+ { "regex": "\\/\\/.*\\n?", "is": "comment" },
{
"regex": "\\/\\*.*?\\*\\/",
"flags": ["dotMatchesNewline"],
@@ -37,7 +37,8 @@
}
},
{ "regex": "[a-zA-Z_][a-zA-Z0-9_]*", "is": "identifier" },
- { "regex": "'[a-zA-Z_][a-zA-Z0-9_]*", "is": "literal" }
+ { "regex": "'[a-zA-Z_][a-zA-Z0-9_]*", "is": "literal" },
+ { "regex": "\n", "is": "" }
],
"keywords": {
"_": { "into": "keyword1" },