diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-11-03 11:44:53 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-11-03 13:35:39 +0100 |
| commit | 37a7afddfaffd44cb9bc013c9506599267e08983 (patch) | |
| tree | 20e7d62d3c5418baff01a21d0406b91bf3096214 /library/src/text/raw.rs | |
| parent | 56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff) | |
Split crates
Diffstat (limited to 'library/src/text/raw.rs')
| -rw-r--r-- | library/src/text/raw.rs | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs new file mode 100644 index 00000000..31f1517e --- /dev/null +++ b/library/src/text/raw.rs @@ -0,0 +1,206 @@ +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, BlockSpacing}; +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; + /// The raw text's font family. + #[property(referenced)] + pub const FAMILY: FontFamily = FontFamily::new("IBM Plex Mono"); + /// The spacing above block-level raw. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into()); + /// The spacing below block-level raw. + #[property(resolve, shorthand(around))] + pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into()); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + Ok(Self { + text: args.expect("text")?, + block: args.named("block")?.unwrap_or(false), + } + .pack()) + } +} + +impl Show for RawNode { + fn unguard_parts(&self, _: Selector) -> Content { + Self { text: self.text.clone(), ..*self }.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, + } + } + + fn realize( + &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(self.text.clone()).pack() + }; + + 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); + + Ok(realized.styled_with_map(map)) + } + + fn finalize( + &self, + _: Tracked<dyn World>, + styles: StyleChain, + mut realized: Content, + ) -> SourceResult<Content> { + let mut map = StyleMap::new(); + map.set_family(styles.get(Self::FAMILY).clone(), styles); + + if self.block { + realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); + } + + 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(piece.into()).pack(); + + 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, + }, + } +} |
