1use std::{io::Write, sync::LazyLock};
11
12use syntect::{
13 easy::HighlightLines, highlighting::FontStyle, parsing::SyntaxSet, util::LinesWithEndings,
14};
15use termcolor::{Color, ColorSpec, WriteColor};
16use two_face::theme::{EmbeddedLazyThemeSet, EmbeddedThemeName};
17use typst_syntax::{
18 ast::{self, AstNode},
19 LinkedNode, Tag,
20};
21
22pub mod ext {
24 pub use syntect;
25 pub use termcolor;
26 pub use typst_syntax;
27}
28
29const ZERO_WIDTH_JOINER: char = '\u{200D}';
30
31#[derive(Debug, thiserror::Error)]
33#[non_exhaustive]
34pub enum Error {
35 #[error(transparent)]
36 Io(#[from] std::io::Error),
37 #[error(transparent)]
38 Syntect(#[from] syntect::Error),
39}
40
41#[derive(Debug, Clone, Copy)]
43pub enum SyntaxMode {
44 Code,
45 Markup,
46 Math,
47}
48
49#[derive(Debug, Clone, Copy)]
50pub struct Highlighter {
51 discord: bool,
52 syntax_mode: SyntaxMode,
53 soft_limit: Option<usize>,
54}
55
56impl Default for Highlighter {
57 fn default() -> Self {
58 Highlighter {
59 discord: false,
60 syntax_mode: SyntaxMode::Markup,
61 soft_limit: None,
62 }
63 }
64}
65
66impl Highlighter {
67 pub fn for_discord(&mut self) -> &mut Self {
75 self.discord = true;
76 self
77 }
78
79 pub fn with_syntax_mode(&mut self, mode: SyntaxMode) -> &mut Self {
83 self.syntax_mode = mode;
84 self
85 }
86
87 pub fn with_soft_limit(&mut self, soft_limit: usize) -> &mut Self {
93 self.soft_limit = Some(soft_limit);
94 self
95 }
96
97 pub fn highlight(&self, input: &str) -> Result<String, Error> {
99 let mut out = termcolor::Ansi::new(Vec::new());
100 self.highlight_to(input, &mut out)?;
101 Ok(String::from_utf8(out.into_inner()).expect("the output should be entirely UTF-8"))
102 }
103
104 pub fn highlight_to<W: WriteColor>(&self, input: &str, out: W) -> Result<(), Error> {
106 let parsed = match self.syntax_mode {
107 SyntaxMode::Code => typst_syntax::parse_code(input),
108 SyntaxMode::Markup => typst_syntax::parse(input),
109 SyntaxMode::Math => typst_syntax::parse_math(input),
110 };
111 let linked = typst_syntax::LinkedNode::new(&parsed);
112 self.highlight_node_to(&linked, out)
113 }
114
115 pub fn highlight_node_to<W: WriteColor>(
123 &self,
124 node: &LinkedNode,
125 mut out: W,
126 ) -> Result<(), Error> {
127 fn inner_highlight_node<W: WriteColor>(
128 highlighter: &Highlighter,
129 hl_level: HighlightLevel,
130 node: &LinkedNode,
131 out: &mut DeferredWriter<W>,
132 color: &mut ColorSpec,
133 ) -> Result<(), Error> {
134 let prev_color = color.clone();
135
136 if let Some(tag) = typst_syntax::highlight(node) {
137 out.set_color(&highlighter.tag_to_color(hl_level, tag))?;
138 }
139
140 if let Some(raw) = ast::Raw::from_untyped(node) {
141 highlighter.highlight_raw(hl_level, out, raw)?;
142 } else if node.text().is_empty() {
143 for child in node.children() {
144 inner_highlight_node(highlighter, hl_level, &child, out, color)?;
145 }
146 } else {
147 write!(out, "{}", node.text())?;
148 }
149
150 out.set_color(&prev_color)?;
151 *color = prev_color;
152
153 Ok(())
154 }
155
156 fn inner<W: WriteColor>(
157 highlighter: &Highlighter,
158 node: &LinkedNode,
159 out: W,
160 hl_level: HighlightLevel,
161 ) -> Result<(), Error> {
162 let mut out = DeferredWriter::new(out);
163 if highlighter.discord {
164 writeln!(out, "```ansi")?;
165 }
166
167 inner_highlight_node(highlighter, hl_level, node, &mut out, &mut ColorSpec::new())?;
168
169 if highlighter.discord {
170 let mut last_leaf = node.clone();
172 while let Some(child) = last_leaf.children().last() {
173 last_leaf = child;
174 }
175 if !last_leaf.text().ends_with('\n') {
176 writeln!(out)?;
177 }
178 writeln!(out, "```")?;
179 }
180 Ok(())
181 }
182
183 if let Some(soft_limit) = self.soft_limit {
184 let mut buf_out = termcolor::Ansi::new(Vec::new());
190 let mut level = HighlightLevel::All;
191 loop {
192 inner(self, node, &mut buf_out, level)?;
193 let mut buf = buf_out.into_inner();
194 if buf.len() < soft_limit || level == HighlightLevel::Off {
195 out.write_all(&buf)?;
196 break;
197 } else {
198 buf.clear();
199 buf_out = termcolor::Ansi::new(buf);
200 level = level.restrict();
201 }
202 }
203 } else {
204 inner(self, node, out, HighlightLevel::All)?;
205 }
206
207 Ok(())
208 }
209
210 fn highlight_raw<W: WriteColor>(
211 &self,
212 hl_level: HighlightLevel,
213 out: &mut DeferredWriter<W>,
214 raw: ast::Raw<'_>,
215 ) -> Result<(), Error> {
216 let text = raw.to_untyped().clone().into_text();
217
218 let backticks: String = text.chars().take_while(|&c| c == '`').collect();
220 let (fence, is_pure_fence, include_content) = {
221 if self.discord && backticks.len() >= 3 {
222 let mut fence: String = backticks
223 .chars()
224 .flat_map(|c| [c, ZERO_WIDTH_JOINER])
225 .collect();
226 fence.pop();
227 (fence, false, true)
228 } else if backticks.len() == 2 {
229 ("`".to_string(), true, false)
230 } else {
231 (backticks, true, true)
232 }
233 };
234
235 if self.discord && !is_pure_fence {
237 out.set_color(&self.tag_to_color(hl_level, Tag::Comment))?;
238 write!(out, "/* when copying, remove and retype these --> */")?;
239 }
240 out.set_color(&self.tag_to_color(hl_level, Tag::Raw))?;
241 write!(out, "{fence}")?;
242
243 if include_content {
244 if let Some(lang) = raw.lang() {
245 write!(out, "{}", lang.get())?;
246 }
247
248 let mut inner = text.trim_start_matches('`');
250 inner = &inner[..inner.len() - (text.len() - inner.len())];
252
253 if let Some(lang) = raw.lang().filter(|_| hl_level >= HighlightLevel::WithRaw) {
254 let lang = lang.get();
255 inner = &inner[lang.len()..]; highlight_lang(inner, lang, out)?;
257 } else {
258 write!(out, "{inner}")?;
259 }
260 }
261
262 out.set_color(&self.tag_to_color(hl_level, Tag::Raw))?;
264 write!(out, "{fence}")?;
265 if self.discord && !is_pure_fence {
266 out.set_color(&self.tag_to_color(hl_level, Tag::Comment))?;
267 write!(out, "/* <-- when copying, remove and retype these */")?;
268 }
269
270 Ok(())
271 }
272
273 fn tag_to_color(&self, hl_level: HighlightLevel, tag: Tag) -> ColorSpec {
274 let mut color = ColorSpec::default();
275 if hl_level == HighlightLevel::Off {
276 return color;
277 }
278
279 let l1 = hl_level >= HighlightLevel::L1;
280 let l2 = hl_level >= HighlightLevel::L2;
281 let l3 = hl_level >= HighlightLevel::L3;
282 let with_styles = hl_level >= HighlightLevel::WithStyles;
283 match tag {
284 Tag::Comment => {
285 if self.discord {
286 color.set_fg(Some(Color::Black))
287 } else {
288 color.set_dimmed(true)
289 }
290 }
291 Tag::Punctuation if l3 => color.set_fg(None),
292 Tag::Escape => color.set_fg(Some(Color::Cyan)),
293 Tag::Strong if l3 => color.set_fg(Some(Color::Yellow)).set_bold(with_styles),
294 Tag::Emph if l3 => color.set_fg(Some(Color::Yellow)).set_italic(with_styles),
295 Tag::Link if l3 => color.set_fg(Some(Color::Blue)).set_underline(with_styles),
296 Tag::Raw if l2 => color.set_fg(Some(Color::White)),
297 Tag::Label if l1 => color.set_fg(Some(Color::Blue)).set_underline(with_styles),
298 Tag::Ref if l1 => color.set_fg(Some(Color::Blue)).set_underline(with_styles),
299 Tag::Heading if l2 => color.set_fg(Some(Color::Cyan)).set_bold(with_styles),
300 Tag::ListMarker if l2 => color.set_fg(Some(Color::Cyan)),
301 Tag::ListTerm if l2 => color.set_fg(Some(Color::Cyan)),
302 Tag::MathDelimiter if l3 => color.set_fg(Some(Color::Cyan)),
303 Tag::MathOperator if l2 => color.set_fg(Some(Color::Cyan)),
304 Tag::Keyword => color.set_fg(Some(Color::Magenta)),
305 Tag::Operator if l3 => color.set_fg(Some(Color::Cyan)),
306 Tag::Number if l1 => color.set_fg(Some(Color::Yellow)),
307 Tag::String if l1 => color.set_fg(Some(Color::Green)),
308 Tag::Function if l3 => color.set_fg(Some(Color::Blue)).set_italic(with_styles),
309 Tag::Interpolated if l3 => color.set_fg(Some(Color::White)),
310 Tag::Error => color.set_fg(Some(Color::Red)),
311 _ => &mut color,
312 };
313 color
314 }
315}
316
317static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(two_face::syntax::extra_newlines);
318static THEME_SET: LazyLock<EmbeddedLazyThemeSet> = LazyLock::new(two_face::theme::extra);
319
320fn highlight_lang<W: WriteColor>(
321 input: &str,
322 lang: &str,
323 out: &mut DeferredWriter<W>,
324) -> Result<(), Error> {
325 let Some(syntax) = SYNTAX_SET.find_syntax_by_token(lang) else {
326 write!(out, "{input}")?;
327 return Ok(());
328 };
329 let ansi_theme = THEME_SET.get(EmbeddedThemeName::Base16);
330
331 let mut highlighter = HighlightLines::new(syntax, ansi_theme);
332 for line in LinesWithEndings::from(input) {
333 let ranges = highlighter.highlight_line(line, &SYNTAX_SET)?;
334 for (styles, text) in ranges {
335 let fg = styles.foreground;
336 let fg = convert_rgb_to_ansi_color(fg.r, fg.g, fg.b, fg.a);
337 let mut color = ColorSpec::new();
338 color.set_fg(fg);
339
340 let font_style = styles.font_style;
341 color.set_bold(font_style.contains(FontStyle::BOLD));
342 color.set_italic(font_style.contains(FontStyle::ITALIC));
343 color.set_underline(font_style.contains(FontStyle::UNDERLINE));
344
345 out.set_color(&color)?;
346 write!(out, "{text}")?;
347 }
348 }
349
350 Ok(())
351}
352
353fn convert_rgb_to_ansi_color(r: u8, g: u8, b: u8, a: u8) -> Option<Color> {
358 match a {
359 0 => Some(match r {
360 0x00 => Color::Black,
362 0x01 => Color::Red,
363 0x02 => Color::Green,
364 0x03 => Color::Yellow,
365 0x04 => Color::Blue,
366 0x05 => Color::Magenta,
367 0x06 => Color::Cyan,
368 0x07 => Color::White,
369 _ => Color::Ansi256(r),
370 }),
371 1 => None,
372 _ => Some(Color::Ansi256(ansi_colours::ansi256_from_rgb((r, g, b)))),
373 }
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
381enum HighlightLevel {
382 Off,
383 L0,
384 L1,
385 L2,
386 L3,
387 WithRaw,
389 WithStyles,
391 All,
392}
393
394impl HighlightLevel {
395 fn restrict(self) -> HighlightLevel {
396 match self {
397 HighlightLevel::Off => HighlightLevel::Off,
398 HighlightLevel::L0 => HighlightLevel::Off,
399 HighlightLevel::L1 => HighlightLevel::L0,
400 HighlightLevel::L2 => HighlightLevel::L1,
401 HighlightLevel::L3 => HighlightLevel::L2,
402 HighlightLevel::WithRaw => HighlightLevel::L3,
403 HighlightLevel::WithStyles => HighlightLevel::WithRaw,
404 HighlightLevel::All => HighlightLevel::WithStyles,
405 }
406 }
407}
408
409struct DeferredWriter<W> {
412 inner: W,
413 current_color: ColorSpec,
414 next_color: Option<ColorSpec>,
415}
416
417impl<W> DeferredWriter<W> {
418 fn new(writer: W) -> DeferredWriter<W> {
419 DeferredWriter {
420 inner: writer,
421 current_color: ColorSpec::new(),
422 next_color: None,
423 }
424 }
425}
426
427impl<W: WriteColor> Write for DeferredWriter<W> {
428 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
429 if let Some(color) = self.next_color.take() {
430 self.inner.set_color(&color)?;
431 self.current_color = color;
432 }
433 self.inner.write(buf)
434 }
435
436 fn flush(&mut self) -> std::io::Result<()> {
437 self.inner.flush()
438 }
439}
440
441impl<W: WriteColor> WriteColor for DeferredWriter<W> {
442 fn supports_color(&self) -> bool {
443 self.inner.supports_color()
444 }
445
446 fn set_color(&mut self, spec: &ColorSpec) -> std::io::Result<()> {
447 if &self.current_color == spec {
448 self.next_color = None;
449 } else {
450 self.next_color = Some(spec.clone());
451 }
452 Ok(())
453 }
454
455 fn reset(&mut self) -> std::io::Result<()> {
456 let mut color = ColorSpec::new();
457 color.set_reset(true);
458 self.next_color = Some(color);
459 Ok(())
460 }
461
462 fn is_synchronous(&self) -> bool {
463 self.inner.is_synchronous()
464 }
465
466 fn set_hyperlink(&mut self, link: &termcolor::HyperlinkSpec) -> std::io::Result<()> {
467 self.inner.set_hyperlink(link)
468 }
469
470 fn supports_hyperlinks(&self) -> bool {
471 self.inner.supports_hyperlinks()
472 }
473}