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" },