mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
175 lines
5.9 KiB
Rust
175 lines
5.9 KiB
Rust
use once_cell::sync::Lazy;
|
|
use syntect::easy::HighlightLines;
|
|
use syntect::highlighting::{
|
|
Color, FontStyle, Style, StyleModifier, Theme, ThemeItem, ThemeSettings,
|
|
};
|
|
use syntect::parsing::SyntaxSet;
|
|
use typst::syntax;
|
|
|
|
use super::{FontFamily, Hyphenate, LinebreakNode, TextNode};
|
|
use crate::layout::BlockNode;
|
|
use crate::prelude::*;
|
|
|
|
/// Monospaced text with optional syntax highlighting.
|
|
#[derive(Debug, Hash)]
|
|
pub struct RawNode {
|
|
/// The raw text.
|
|
pub text: EcoString,
|
|
/// Whether the node is block-level.
|
|
pub block: bool,
|
|
}
|
|
|
|
#[node(Show)]
|
|
impl RawNode {
|
|
/// The language to syntax-highlight in.
|
|
#[property(referenced)]
|
|
pub const LANG: Option<EcoString> = None;
|
|
|
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
|
Ok(Self {
|
|
text: args.expect("text")?,
|
|
block: args.named("block")?.unwrap_or(false),
|
|
}
|
|
.pack())
|
|
}
|
|
|
|
fn field(&self, name: &str) -> Option<Value> {
|
|
match name {
|
|
"text" => Some(Value::Str(self.text.clone().into())),
|
|
"block" => Some(Value::Bool(self.block)),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Show for RawNode {
|
|
fn show(&self, _: Tracked<dyn World>, styles: StyleChain) -> SourceResult<Content> {
|
|
let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase());
|
|
let foreground = THEME
|
|
.settings
|
|
.foreground
|
|
.map(Color::from)
|
|
.unwrap_or(Color::BLACK)
|
|
.into();
|
|
|
|
let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) {
|
|
let root = match lang.as_deref() {
|
|
Some("typc") => syntax::parse_code(&self.text),
|
|
_ => syntax::parse(&self.text),
|
|
};
|
|
|
|
let mut seq = vec![];
|
|
syntax::highlight::highlight_themed(&root, &THEME, |range, style| {
|
|
seq.push(styled(&self.text[range], foreground, style));
|
|
});
|
|
|
|
Content::sequence(seq)
|
|
} else if let Some(syntax) =
|
|
lang.and_then(|token| SYNTAXES.find_syntax_by_token(&token))
|
|
{
|
|
let mut seq = vec![];
|
|
let mut highlighter = HighlightLines::new(syntax, &THEME);
|
|
for (i, line) in self.text.lines().enumerate() {
|
|
if i != 0 {
|
|
seq.push(LinebreakNode { justify: false }.pack());
|
|
}
|
|
|
|
for (style, piece) in
|
|
highlighter.highlight_line(line, &SYNTAXES).into_iter().flatten()
|
|
{
|
|
seq.push(styled(piece, foreground, style));
|
|
}
|
|
}
|
|
|
|
Content::sequence(seq)
|
|
} else {
|
|
TextNode::packed(self.text.clone())
|
|
};
|
|
|
|
if self.block {
|
|
realized = BlockNode(realized).pack();
|
|
}
|
|
|
|
let mut map = StyleMap::new();
|
|
map.set(TextNode::OVERHANG, false);
|
|
map.set(TextNode::HYPHENATE, Hyphenate(Smart::Custom(false)));
|
|
map.set(TextNode::SMART_QUOTES, false);
|
|
map.set_family(FontFamily::new("IBM Plex Mono"), styles);
|
|
|
|
Ok(realized.styled_with_map(map))
|
|
}
|
|
}
|
|
|
|
/// Style a piece of text with a syntect style.
|
|
fn styled(piece: &str, foreground: Paint, style: Style) -> Content {
|
|
let mut body = TextNode::packed(piece);
|
|
|
|
let paint = style.foreground.into();
|
|
if paint != foreground {
|
|
body = body.styled(TextNode::FILL, paint);
|
|
}
|
|
|
|
if style.font_style.contains(FontStyle::BOLD) {
|
|
body = body.strong();
|
|
}
|
|
|
|
if style.font_style.contains(FontStyle::ITALIC) {
|
|
body = body.emph();
|
|
}
|
|
|
|
if style.font_style.contains(FontStyle::UNDERLINE) {
|
|
body = body.underlined();
|
|
}
|
|
|
|
body
|
|
}
|
|
|
|
/// The syntect syntax definitions.
|
|
static SYNTAXES: Lazy<SyntaxSet> = Lazy::new(|| SyntaxSet::load_defaults_newlines());
|
|
|
|
/// The default theme used for syntax highlighting.
|
|
#[rustfmt::skip]
|
|
pub static THEME: Lazy<Theme> = Lazy::new(|| Theme {
|
|
name: Some("Typst Light".into()),
|
|
author: Some("The Typst Project Developers".into()),
|
|
settings: ThemeSettings::default(),
|
|
scopes: vec![
|
|
item("comment", Some("#8a8a8a"), None),
|
|
item("constant.character.escape", Some("#1d6c76"), None),
|
|
item("constant.character.shortcut", Some("#1d6c76"), None),
|
|
item("markup.bold", None, Some(FontStyle::BOLD)),
|
|
item("markup.italic", None, Some(FontStyle::ITALIC)),
|
|
item("markup.underline", None, Some(FontStyle::UNDERLINE)),
|
|
item("markup.raw", Some("#818181"), None),
|
|
item("string.other.math.typst", None, None),
|
|
item("punctuation.definition.math", Some("#298e0d"), None),
|
|
item("keyword.operator.math", Some("#1d6c76"), None),
|
|
item("markup.heading, entity.name.section", None, Some(FontStyle::BOLD)),
|
|
item("markup.heading.typst", None, Some(FontStyle::BOLD | FontStyle::UNDERLINE)),
|
|
item("punctuation.definition.list", Some("#8b41b1"), None),
|
|
item("markup.list.term", None, Some(FontStyle::BOLD)),
|
|
item("entity.name.label, markup.other.reference", Some("#1d6c76"), None),
|
|
item("keyword, constant.language, variable.language", Some("#d73a49"), None),
|
|
item("storage.type, storage.modifier", Some("#d73a49"), None),
|
|
item("constant", Some("#b60157"), None),
|
|
item("string", Some("#298e0d"), None),
|
|
item("entity.name, variable.function, support", Some("#4b69c6"), None),
|
|
item("support.macro", Some("#16718d"), None),
|
|
item("meta.annotation", Some("#301414"), None),
|
|
item("entity.other, meta.interpolation", Some("#8b41b1"), None),
|
|
item("invalid", Some("#ff0000"), None),
|
|
],
|
|
});
|
|
|
|
/// Create a syntect theme item.
|
|
fn item(scope: &str, color: Option<&str>, font_style: Option<FontStyle>) -> ThemeItem {
|
|
ThemeItem {
|
|
scope: scope.parse().unwrap(),
|
|
style: StyleModifier {
|
|
foreground: color.map(|s| s.parse::<RgbaColor>().unwrap().into()),
|
|
background: None,
|
|
font_style,
|
|
},
|
|
}
|
|
}
|