summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/text/raw.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/text/raw.rs')
-rw-r--r--crates/typst-library/src/text/raw.rs926
1 files changed, 926 insertions, 0 deletions
diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs
new file mode 100644
index 00000000..5ce77348
--- /dev/null
+++ b/crates/typst-library/src/text/raw.rs
@@ -0,0 +1,926 @@
+use std::hash::Hash;
+use std::ops::Range;
+use std::sync::Arc;
+
+use ecow::{eco_format, EcoString, EcoVec};
+use once_cell::sync::Lazy;
+use once_cell::unsync::Lazy as UnsyncLazy;
+use syntect::highlighting::{self as synt, Theme};
+use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
+use typst_syntax::{split_newlines, LinkedNode, Span, Spanned};
+use unicode_segmentation::UnicodeSegmentation;
+
+use super::Lang;
+use crate::diag::{At, FileError, HintedStrResult, SourceResult, StrResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed,
+ PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value,
+};
+use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
+use crate::model::{Figurable, ParElem};
+use crate::text::{
+ FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize,
+};
+use crate::visualize::Color;
+use crate::World;
+
+// Shorthand for highlighter closures.
+type StyleFn<'a> =
+ &'a mut dyn FnMut(usize, &LinkedNode, Range<usize>, synt::Style) -> Content;
+type LineFn<'a> = &'a mut dyn FnMut(usize, Range<usize>, &mut Vec<Content>);
+type ThemeArgType = Smart<Option<EcoString>>;
+
+/// Raw text with optional syntax highlighting.
+///
+/// Displays the text verbatim and in a monospace font. This is typically used
+/// to embed computer code into your document.
+///
+/// # Example
+/// ````example
+/// Adding `rbx` to `rcx` gives
+/// the desired result.
+///
+/// What is ```rust fn main()``` in Rust
+/// would be ```c int main()``` in C.
+///
+/// ```rust
+/// fn main() {
+/// println!("Hello World!");
+/// }
+/// ```
+///
+/// This has ``` `backticks` ``` in it
+/// (but the spaces are trimmed). And
+/// ``` here``` the leading space is
+/// also trimmed.
+/// ````
+///
+/// You can also construct a [`raw`] element programmatically from a string (and
+/// provide the language tag via the optional [`lang`]($raw.lang) argument).
+/// ```example
+/// #raw("fn " + "main() {}", lang: "rust")
+/// ```
+///
+/// # Syntax
+/// This function also has dedicated syntax. You can enclose text in 1 or 3+
+/// backticks (`` ` ``) to make it raw. Two backticks produce empty raw text.
+/// This works both in markup and code.
+///
+/// When you use three or more backticks, you can additionally specify a
+/// language tag for syntax highlighting directly after the opening backticks.
+/// Within raw blocks, everything (except for the language tag, if applicable)
+/// is rendered as is, in particular, there are no escape sequences.
+///
+/// The language tag is an identifier that directly follows the opening
+/// backticks only if there are three or more backticks. If your text starts
+/// with something that looks like an identifier, but no syntax highlighting is
+/// needed, start the text with a single space (which will be trimmed) or use
+/// the single backtick syntax. If your text should start or end with a
+/// backtick, put a space before or after it (it will be trimmed).
+#[elem(
+ scope,
+ title = "Raw Text / Code",
+ Synthesize,
+ Show,
+ ShowSet,
+ LocalName,
+ Figurable,
+ PlainText
+)]
+pub struct RawElem {
+ /// The raw text.
+ ///
+ /// You can also use raw blocks creatively to create custom syntaxes for
+ /// your automations.
+ ///
+ /// ````example
+ /// // Parse numbers in raw blocks with the
+ /// // `mydsl` tag and sum them up.
+ /// #show raw.where(lang: "mydsl"): it => {
+ /// let sum = 0
+ /// for part in it.text.split("+") {
+ /// sum += int(part.trim())
+ /// }
+ /// sum
+ /// }
+ ///
+ /// ```mydsl
+ /// 1 + 2 + 3 + 4 + 5
+ /// ```
+ /// ````
+ #[required]
+ pub text: RawContent,
+
+ /// Whether the raw text is displayed as a separate block.
+ ///
+ /// In markup mode, using one-backtick notation makes this `{false}`.
+ /// Using three-backtick notation makes it `{true}` if the enclosed content
+ /// contains at least one line break.
+ ///
+ /// ````example
+ /// // Display inline code in a small box
+ /// // that retains the correct baseline.
+ /// #show raw.where(block: false): box.with(
+ /// fill: luma(240),
+ /// inset: (x: 3pt, y: 0pt),
+ /// outset: (y: 3pt),
+ /// radius: 2pt,
+ /// )
+ ///
+ /// // Display block code in a larger block
+ /// // with more padding.
+ /// #show raw.where(block: true): block.with(
+ /// fill: luma(240),
+ /// inset: 10pt,
+ /// radius: 4pt,
+ /// )
+ ///
+ /// With `rg`, you can search through your files quickly.
+ /// This example searches the current directory recursively
+ /// for the text `Hello World`:
+ ///
+ /// ```bash
+ /// rg "Hello World"
+ /// ```
+ /// ````
+ #[default(false)]
+ pub block: bool,
+
+ /// The language to syntax-highlight in.
+ ///
+ /// Apart from typical language tags known from Markdown, this supports the
+ /// `{"typ"}`, `{"typc"}`, and `{"typm"}` tags for
+ /// [Typst markup]($reference/syntax/#markup),
+ /// [Typst code]($reference/syntax/#code), and
+ /// [Typst math]($reference/syntax/#math), respectively.
+ ///
+ /// ````example
+ /// ```typ
+ /// This is *Typst!*
+ /// ```
+ ///
+ /// This is ```typ also *Typst*```, but inline!
+ /// ````
+ #[borrowed]
+ pub lang: Option<EcoString>,
+
+ /// The horizontal alignment that each line in a raw block should have.
+ /// This option is ignored if this is not a raw block (if specified
+ /// `block: false` or single backticks were used in markup mode).
+ ///
+ /// By default, this is set to `{start}`, meaning that raw text is
+ /// aligned towards the start of the text direction inside the block
+ /// by default, regardless of the current context's alignment (allowing
+ /// you to center the raw block itself without centering the text inside
+ /// it, for example).
+ ///
+ /// ````example
+ /// #set raw(align: center)
+ ///
+ /// ```typc
+ /// let f(x) = x
+ /// code = "centered"
+ /// ```
+ /// ````
+ #[default(HAlignment::Start)]
+ pub align: HAlignment,
+
+ /// One or multiple additional syntax definitions to load. The syntax
+ /// definitions should be in the
+ /// [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html).
+ ///
+ /// ````example
+ /// #set raw(syntaxes: "SExpressions.sublime-syntax")
+ ///
+ /// ```sexp
+ /// (defun factorial (x)
+ /// (if (zerop x)
+ /// ; with a comment
+ /// 1
+ /// (* x (factorial (- x 1)))))
+ /// ```
+ /// ````
+ #[parse(
+ let (syntaxes, syntaxes_data) = parse_syntaxes(engine, args)?;
+ syntaxes
+ )]
+ #[fold]
+ pub syntaxes: SyntaxPaths,
+
+ /// The raw file buffers of syntax definition files.
+ #[internal]
+ #[parse(syntaxes_data)]
+ #[fold]
+ pub syntaxes_data: Vec<Bytes>,
+
+ /// The theme to use for syntax highlighting. Theme files should be in the
+ /// [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html).
+ ///
+ /// Applying a theme only affects the color of specifically highlighted
+ /// text. It does not consider the theme's foreground and background
+ /// properties, so that you retain control over the color of raw text. You
+ /// can apply the foreground color yourself with the [`text`] function and
+ /// the background with a [filled block]($block.fill). You could also use
+ /// the [`xml`] function to extract these properties from the theme.
+ ///
+ /// Additionally, you can set the theme to `{none}` to disable highlighting.
+ ///
+ /// ````example
+ /// #set raw(theme: "halcyon.tmTheme")
+ /// #show raw: it => block(
+ /// fill: rgb("#1d2433"),
+ /// inset: 8pt,
+ /// radius: 5pt,
+ /// text(fill: rgb("#a2aabc"), it)
+ /// )
+ ///
+ /// ```typ
+ /// = Chapter 1
+ /// #let hi = "Hello World"
+ /// ```
+ /// ````
+ #[parse(
+ let (theme_path, theme_data) = parse_theme(engine, args)?;
+ theme_path
+ )]
+ #[borrowed]
+ pub theme: ThemeArgType,
+
+ /// The raw file buffer of syntax theme file.
+ #[internal]
+ #[parse(theme_data.map(Some))]
+ #[borrowed]
+ pub theme_data: Option<Bytes>,
+
+ /// The size for a tab stop in spaces. A tab is replaced with enough spaces to
+ /// align with the next multiple of the size.
+ ///
+ /// ````example
+ /// #set raw(tab-size: 8)
+ /// ```tsv
+ /// Year Month Day
+ /// 2000 2 3
+ /// 2001 2 1
+ /// 2002 3 10
+ /// ```
+ /// ````
+ #[default(2)]
+ pub tab_size: usize,
+
+ /// The stylized lines of raw text.
+ ///
+ /// Made accessible for the [`raw.line` element]($raw.line).
+ /// Allows more styling control in `show` rules.
+ #[synthesized]
+ pub lines: Vec<Packed<RawLine>>,
+}
+
+#[scope]
+impl RawElem {
+ #[elem]
+ type RawLine;
+}
+
+impl RawElem {
+ /// The supported language names and tags.
+ pub fn languages() -> Vec<(&'static str, Vec<&'static str>)> {
+ RAW_SYNTAXES
+ .syntaxes()
+ .iter()
+ .map(|syntax| {
+ (
+ syntax.name.as_str(),
+ syntax.file_extensions.iter().map(|s| s.as_str()).collect(),
+ )
+ })
+ .chain([
+ ("Typst", vec!["typ"]),
+ ("Typst (code)", vec!["typc"]),
+ ("Typst (math)", vec!["typm"]),
+ ])
+ .collect()
+ }
+}
+
+impl Synthesize for Packed<RawElem> {
+ fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
+ let seq = self.highlight(styles);
+ self.push_lines(seq);
+ Ok(())
+ }
+}
+
+impl Packed<RawElem> {
+ #[comemo::memoize]
+ fn highlight(&self, styles: StyleChain) -> Vec<Packed<RawLine>> {
+ let elem = self.as_ref();
+ let lines = preprocess(elem.text(), styles, self.span());
+
+ let count = lines.len() as i64;
+ let lang = elem
+ .lang(styles)
+ .as_ref()
+ .as_ref()
+ .map(|s| s.to_lowercase())
+ .or(Some("txt".into()));
+
+ let extra_syntaxes = UnsyncLazy::new(|| {
+ load_syntaxes(&elem.syntaxes(styles), &elem.syntaxes_data(styles)).unwrap()
+ });
+ let non_highlighted_result = |lines: EcoVec<(EcoString, Span)>| {
+ lines.into_iter().enumerate().map(|(i, (line, line_span))| {
+ Packed::new(RawLine::new(
+ i as i64 + 1,
+ count,
+ line.clone(),
+ TextElem::packed(line).spanned(line_span),
+ ))
+ .spanned(line_span)
+ })
+ };
+
+ let theme = elem.theme(styles).as_ref().as_ref().map(|theme_path| {
+ theme_path.as_ref().map(|path| {
+ load_theme(path, elem.theme_data(styles).as_ref().as_ref().unwrap())
+ .unwrap()
+ })
+ });
+ let theme: &Theme = match theme {
+ Smart::Auto => &RAW_THEME,
+ Smart::Custom(Some(ref theme)) => theme,
+ Smart::Custom(None) => return non_highlighted_result(lines).collect(),
+ };
+ let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK);
+
+ let mut seq = vec![];
+ if matches!(lang.as_deref(), Some("typ" | "typst" | "typc" | "typm")) {
+ let text =
+ lines.iter().map(|(s, _)| s.clone()).collect::<Vec<_>>().join("\n");
+ let root = match lang.as_deref() {
+ Some("typc") => typst_syntax::parse_code(&text),
+ Some("typm") => typst_syntax::parse_math(&text),
+ _ => typst_syntax::parse(&text),
+ };
+
+ ThemedHighlighter::new(
+ &text,
+ LinkedNode::new(&root),
+ synt::Highlighter::new(theme),
+ &mut |i, _, range, style| {
+ // Find span and start of line.
+ // Note: Dedent is already applied to the text
+ let span = lines.get(i).map_or_else(Span::detached, |l| l.1);
+ let span_offset = text[..range.start]
+ .rfind('\n')
+ .map_or(0, |i| range.start - (i + 1));
+ styled(&text[range], foreground, style, span, span_offset)
+ },
+ &mut |i, range, line| {
+ let span = lines.get(i).map_or_else(Span::detached, |l| l.1);
+ seq.push(
+ Packed::new(RawLine::new(
+ (i + 1) as i64,
+ count,
+ EcoString::from(&text[range]),
+ Content::sequence(line.drain(..)),
+ ))
+ .spanned(span),
+ );
+ },
+ )
+ .highlight();
+ } else if let Some((syntax_set, syntax)) = lang.and_then(|token| {
+ RAW_SYNTAXES
+ .find_syntax_by_token(&token)
+ .map(|syntax| (&*RAW_SYNTAXES, syntax))
+ .or_else(|| {
+ extra_syntaxes
+ .find_syntax_by_token(&token)
+ .map(|syntax| (&**extra_syntaxes, syntax))
+ })
+ }) {
+ let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme);
+ for (i, (line, line_span)) in lines.into_iter().enumerate() {
+ let mut line_content = vec![];
+ let mut span_offset = 0;
+ for (style, piece) in highlighter
+ .highlight_line(line.as_str(), syntax_set)
+ .into_iter()
+ .flatten()
+ {
+ line_content.push(styled(
+ piece,
+ foreground,
+ style,
+ line_span,
+ span_offset,
+ ));
+ span_offset += piece.len();
+ }
+
+ seq.push(
+ Packed::new(RawLine::new(
+ i as i64 + 1,
+ count,
+ line,
+ Content::sequence(line_content),
+ ))
+ .spanned(line_span),
+ );
+ }
+ } else {
+ seq.extend(non_highlighted_result(lines));
+ };
+
+ seq
+ }
+}
+
+impl Show for Packed<RawElem> {
+ #[typst_macros::time(name = "raw", span = self.span())]
+ fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ let lines = self.lines().map(|v| v.as_slice()).unwrap_or_default();
+
+ let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1));
+ for (i, line) in lines.iter().enumerate() {
+ if i != 0 {
+ seq.push(LinebreakElem::shared().clone());
+ }
+
+ seq.push(line.clone().pack());
+ }
+
+ let mut realized = Content::sequence(seq);
+ if self.block(styles) {
+ // Align the text before inserting it into the block.
+ realized = realized.aligned(self.align(styles).into());
+ realized = BlockElem::new()
+ .with_body(Some(BlockBody::Content(realized)))
+ .pack()
+ .spanned(self.span());
+ }
+
+ Ok(realized)
+ }
+}
+
+impl ShowSet for Packed<RawElem> {
+ fn show_set(&self, styles: StyleChain) -> Styles {
+ let mut out = Styles::new();
+ out.set(TextElem::set_overhang(false));
+ out.set(TextElem::set_lang(Lang::ENGLISH));
+ out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
+ out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
+ out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
+ if self.block(styles) {
+ out.set(ParElem::set_shrink(false));
+ }
+ out
+ }
+}
+
+impl LocalName for Packed<RawElem> {
+ const KEY: &'static str = "raw";
+}
+
+impl Figurable for Packed<RawElem> {}
+
+impl PlainText for Packed<RawElem> {
+ fn plain_text(&self, text: &mut EcoString) {
+ text.push_str(&self.text().get());
+ }
+}
+
+/// The content of the raw text.
+#[derive(Debug, Clone, Hash, PartialEq)]
+pub enum RawContent {
+ /// From a string.
+ Text(EcoString),
+ /// From lines of text.
+ Lines(EcoVec<(EcoString, Span)>),
+}
+
+impl RawContent {
+ /// Returns or synthesizes the text content of the raw text.
+ fn get(&self) -> EcoString {
+ match self.clone() {
+ RawContent::Text(text) => text,
+ RawContent::Lines(lines) => {
+ let mut lines = lines.into_iter().map(|(s, _)| s);
+ if lines.len() <= 1 {
+ lines.next().unwrap_or_default()
+ } else {
+ lines.collect::<Vec<_>>().join("\n").into()
+ }
+ }
+ }
+ }
+}
+
+cast! {
+ RawContent,
+ self => self.get().into_value(),
+ v: EcoString => Self::Text(v),
+}
+
+/// A highlighted line of raw text.
+///
+/// This is a helper element that is synthesized by [`raw`] elements.
+///
+/// It allows you to access various properties of the line, such as the line
+/// number, the raw non-highlighted text, the highlighted text, and whether it
+/// is the first or last line of the raw block.
+#[elem(name = "line", title = "Raw Text / Code Line", Show, PlainText)]
+pub struct RawLine {
+ /// The line number of the raw line inside of the raw block, starts at 1.
+ #[required]
+ pub number: i64,
+
+ /// The total number of lines in the raw block.
+ #[required]
+ pub count: i64,
+
+ /// The line of raw text.
+ #[required]
+ pub text: EcoString,
+
+ /// The highlighted raw text.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for Packed<RawLine> {
+ #[typst_macros::time(name = "raw.line", span = self.span())]
+ fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult<Content> {
+ Ok(self.body().clone())
+ }
+}
+
+impl PlainText for Packed<RawLine> {
+ fn plain_text(&self, text: &mut EcoString) {
+ text.push_str(self.text());
+ }
+}
+
+/// Wrapper struct for the state required to highlight typst code.
+struct ThemedHighlighter<'a> {
+ /// The code being highlighted.
+ code: &'a str,
+ /// The current node being highlighted.
+ node: LinkedNode<'a>,
+ /// The highlighter.
+ highlighter: synt::Highlighter<'a>,
+ /// The current scopes.
+ scopes: Vec<syntect::parsing::Scope>,
+ /// The current highlighted line.
+ current_line: Vec<Content>,
+ /// The range of the current line.
+ range: Range<usize>,
+ /// The current line number.
+ line: usize,
+ /// The function to style a piece of text.
+ style_fn: StyleFn<'a>,
+ /// The function to append a line.
+ line_fn: LineFn<'a>,
+}
+
+impl<'a> ThemedHighlighter<'a> {
+ pub fn new(
+ code: &'a str,
+ top: LinkedNode<'a>,
+ highlighter: synt::Highlighter<'a>,
+ style_fn: StyleFn<'a>,
+ line_fn: LineFn<'a>,
+ ) -> Self {
+ Self {
+ code,
+ node: top,
+ highlighter,
+ range: 0..0,
+ scopes: Vec::new(),
+ current_line: Vec::new(),
+ line: 0,
+ style_fn,
+ line_fn,
+ }
+ }
+
+ pub fn highlight(&mut self) {
+ self.highlight_inner();
+
+ if !self.current_line.is_empty() {
+ (self.line_fn)(
+ self.line,
+ self.range.start..self.code.len(),
+ &mut self.current_line,
+ );
+
+ self.current_line.clear();
+ }
+ }
+
+ fn highlight_inner(&mut self) {
+ if self.node.children().len() == 0 {
+ let style = self.highlighter.style_for_stack(&self.scopes);
+ let segment = &self.code[self.node.range()];
+
+ let mut len = 0;
+ for (i, line) in split_newlines(segment).into_iter().enumerate() {
+ if i != 0 {
+ (self.line_fn)(
+ self.line,
+ self.range.start..self.range.end + len - 1,
+ &mut self.current_line,
+ );
+ self.range.start = self.range.end + len;
+ self.line += 1;
+ }
+
+ let offset = self.node.range().start + len;
+ let token_range = offset..(offset + line.len());
+ self.current_line.push((self.style_fn)(
+ self.line,
+ &self.node,
+ token_range,
+ style,
+ ));
+
+ len += line.len() + 1;
+ }
+
+ self.range.end += segment.len();
+ }
+
+ for child in self.node.children() {
+ let mut scopes = self.scopes.clone();
+ if let Some(tag) = typst_syntax::highlight(&child) {
+ scopes.push(syntect::parsing::Scope::new(tag.tm_scope()).unwrap())
+ }
+
+ std::mem::swap(&mut scopes, &mut self.scopes);
+ self.node = child;
+ self.highlight_inner();
+ std::mem::swap(&mut scopes, &mut self.scopes);
+ }
+ }
+}
+
+fn preprocess(
+ text: &RawContent,
+ styles: StyleChain,
+ span: Span,
+) -> EcoVec<(EcoString, Span)> {
+ if let RawContent::Lines(lines) = text {
+ if lines.iter().all(|(s, _)| !s.contains('\t')) {
+ return lines.clone();
+ }
+ }
+
+ let mut text = text.get();
+ if text.contains('\t') {
+ let tab_size = RawElem::tab_size_in(styles);
+ text = align_tabs(&text, tab_size);
+ }
+ split_newlines(&text)
+ .into_iter()
+ .map(|line| (line.into(), span))
+ .collect()
+}
+
+/// Style a piece of text with a syntect style.
+fn styled(
+ piece: &str,
+ foreground: synt::Color,
+ style: synt::Style,
+ span: Span,
+ span_offset: usize,
+) -> Content {
+ let mut body = TextElem::packed(piece).spanned(span);
+
+ if span_offset > 0 {
+ body = body.styled(TextElem::set_span_offset(span_offset));
+ }
+
+ if style.foreground != foreground {
+ body = body.styled(TextElem::set_fill(to_typst(style.foreground).into()));
+ }
+
+ if style.font_style.contains(synt::FontStyle::BOLD) {
+ body = body.strong().spanned(span);
+ }
+
+ if style.font_style.contains(synt::FontStyle::ITALIC) {
+ body = body.emph().spanned(span);
+ }
+
+ if style.font_style.contains(synt::FontStyle::UNDERLINE) {
+ body = body.underlined().spanned(span);
+ }
+
+ body
+}
+
+fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> Color {
+ Color::from_u8(r, g, b, a)
+}
+
+fn to_syn(color: Color) -> synt::Color {
+ let [r, g, b, a] = color.to_rgb().to_vec4_u8();
+ synt::Color { r, g, b, a }
+}
+
+/// A list of raw syntax file paths.
+#[derive(Debug, Default, Clone, PartialEq, Hash)]
+pub struct SyntaxPaths(Vec<EcoString>);
+
+cast! {
+ SyntaxPaths,
+ self => self.0.into_value(),
+ v: EcoString => Self(vec![v]),
+ v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
+}
+
+impl Fold for SyntaxPaths {
+ fn fold(self, outer: Self) -> Self {
+ Self(self.0.fold(outer.0))
+ }
+}
+
+/// Load a syntax set from a list of syntax file paths.
+#[comemo::memoize]
+#[typst_macros::time(name = "load syntaxes")]
+fn load_syntaxes(paths: &SyntaxPaths, bytes: &[Bytes]) -> StrResult<Arc<SyntaxSet>> {
+ let mut out = SyntaxSetBuilder::new();
+
+ // We might have multiple sublime-syntax/yaml files
+ for (path, bytes) in paths.0.iter().zip(bytes.iter()) {
+ let src = std::str::from_utf8(bytes).map_err(FileError::from)?;
+ out.add(SyntaxDefinition::load_from_str(src, false, None).map_err(|err| {
+ eco_format!("failed to parse syntax file `{path}` ({err})")
+ })?);
+ }
+
+ Ok(Arc::new(out.build()))
+}
+
+/// Function to parse the syntaxes argument.
+/// Much nicer than having it be part of the `element` macro.
+fn parse_syntaxes(
+ engine: &mut Engine,
+ args: &mut Args,
+) -> SourceResult<(Option<SyntaxPaths>, Option<Vec<Bytes>>)> {
+ let Some(Spanned { v: paths, span }) =
+ args.named::<Spanned<SyntaxPaths>>("syntaxes")?
+ else {
+ return Ok((None, None));
+ };
+
+ // Load syntax files.
+ let data = paths
+ .0
+ .iter()
+ .map(|path| {
+ let id = span.resolve_path(path).at(span)?;
+ engine.world.file(id).at(span)
+ })
+ .collect::<SourceResult<Vec<Bytes>>>()?;
+
+ // Check that parsing works.
+ let _ = load_syntaxes(&paths, &data).at(span)?;
+
+ Ok((Some(paths), Some(data)))
+}
+
+#[comemo::memoize]
+#[typst_macros::time(name = "load theme")]
+fn load_theme(path: &str, bytes: &Bytes) -> StrResult<Arc<synt::Theme>> {
+ let mut cursor = std::io::Cursor::new(bytes.as_slice());
+
+ synt::ThemeSet::load_from_reader(&mut cursor)
+ .map(Arc::new)
+ .map_err(|err| eco_format!("failed to parse theme file `{path}` ({err})"))
+}
+
+/// Function to parse the theme argument.
+/// Much nicer than having it be part of the `element` macro.
+fn parse_theme(
+ engine: &mut Engine,
+ args: &mut Args,
+) -> SourceResult<(Option<ThemeArgType>, Option<Bytes>)> {
+ let Some(Spanned { v: path, span }) = args.named::<Spanned<ThemeArgType>>("theme")?
+ else {
+ // Argument `theme` not found.
+ return Ok((None, None));
+ };
+
+ let Smart::Custom(path) = path else {
+ // Argument `theme` is `auto`.
+ return Ok((Some(Smart::Auto), None));
+ };
+
+ let Some(path) = path else {
+ // Argument `theme` is `none`.
+ return Ok((Some(Smart::Custom(None)), None));
+ };
+
+ // Load theme file.
+ let id = span.resolve_path(&path).at(span)?;
+ let data = engine.world.file(id).at(span)?;
+
+ // Check that parsing works.
+ let _ = load_theme(&path, &data).at(span)?;
+
+ Ok((Some(Smart::Custom(Some(path))), Some(data)))
+}
+
+/// The syntect syntax definitions.
+///
+/// Syntax set is generated from the syntaxes from the `bat` project
+/// <https://github.com/sharkdp/bat/tree/master/assets/syntaxes>
+pub static RAW_SYNTAXES: Lazy<syntect::parsing::SyntaxSet> =
+ Lazy::new(two_face::syntax::extra_no_newlines);
+
+/// The default theme used for syntax highlighting.
+pub static RAW_THEME: Lazy<synt::Theme> = Lazy::new(|| synt::Theme {
+ name: Some("Typst Light".into()),
+ author: Some("The Typst Project Developers".into()),
+ settings: synt::ThemeSettings::default(),
+ scopes: vec![
+ item("comment", Some("#8a8a8a"), None),
+ item("constant.character.escape", Some("#1d6c76"), None),
+ item("markup.bold", None, Some(synt::FontStyle::BOLD)),
+ item("markup.italic", None, Some(synt::FontStyle::ITALIC)),
+ item("markup.underline", None, Some(synt::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(synt::FontStyle::BOLD)),
+ item(
+ "markup.heading.typst",
+ None,
+ Some(synt::FontStyle::BOLD | synt::FontStyle::UNDERLINE),
+ ),
+ item("punctuation.definition.list", Some("#8b41b1"), None),
+ item("markup.list.term", None, Some(synt::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("meta.diff.range", Some("#8b41b1"), None),
+ item("markup.inserted, meta.diff.header.to-file", Some("#298e0d"), None),
+ item("markup.deleted, meta.diff.header.from-file", Some("#d73a49"), None),
+ ],
+});
+
+/// Create a syntect theme item.
+fn item(
+ scope: &str,
+ color: Option<&str>,
+ font_style: Option<synt::FontStyle>,
+) -> synt::ThemeItem {
+ synt::ThemeItem {
+ scope: scope.parse().unwrap(),
+ style: synt::StyleModifier {
+ foreground: color.map(|s| to_syn(s.parse::<Color>().unwrap())),
+ background: None,
+ font_style,
+ },
+ }
+}
+
+/// Replace tabs with spaces to align with multiples of `tab_size`.
+fn align_tabs(text: &str, tab_size: usize) -> EcoString {
+ let replacement = " ".repeat(tab_size);
+ let divisor = tab_size.max(1);
+ let amount = text.chars().filter(|&c| c == '\t').count();
+
+ let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size);
+ let mut column = 0;
+
+ for grapheme in text.graphemes(true) {
+ match grapheme {
+ "\t" => {
+ let required = tab_size - column % divisor;
+ res.push_str(&replacement[..required]);
+ column += required;
+ }
+ "\n" => {
+ res.push_str(grapheme);
+ column = 0;
+ }
+ _ => {
+ res.push_str(grapheme);
+ column += 1;
+ }
+ }
+ }
+
+ res
+}