mirror of
https://github.com/typst/typst
synced 2025-07-27 14:27:56 +08:00
904 lines
29 KiB
Rust
904 lines
29 KiB
Rust
use std::cell::LazyCell;
|
|
use std::ops::Range;
|
|
use std::sync::{Arc, LazyLock};
|
|
|
|
use comemo::Tracked;
|
|
use ecow::{EcoString, EcoVec};
|
|
use syntect::highlighting::{self as synt};
|
|
use syntect::parsing::{ParseSyntaxError, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
|
|
use typst_syntax::{split_newlines, LinkedNode, Span, Spanned};
|
|
use typst_utils::ManuallyHash;
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
|
|
use super::Lang;
|
|
use crate::diag::{
|
|
LineCol, LoadError, LoadResult, LoadedWithin, ReportPos, SourceResult,
|
|
};
|
|
use crate::engine::Engine;
|
|
use crate::foundations::{
|
|
cast, elem, scope, Bytes, Content, Derived, OneOrMultiple, Packed, PlainText,
|
|
ShowSet, Smart, StyleChain, Styles, Synthesize,
|
|
};
|
|
use crate::introspection::Locatable;
|
|
use crate::layout::{Em, HAlignment};
|
|
use crate::loading::{DataSource, Load};
|
|
use crate::model::{Figurable, ParElem};
|
|
use crate::text::{FontFamily, FontList, LocalName, TextElem, TextSize};
|
|
use crate::visualize::Color;
|
|
use crate::World;
|
|
|
|
/// 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,
|
|
Locatable,
|
|
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!
|
|
/// ````
|
|
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,
|
|
|
|
/// Additional syntax definitions to load. The syntax definitions should be
|
|
/// in the [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html).
|
|
///
|
|
/// You can pass any of the following values:
|
|
///
|
|
/// - A path string to load a syntax file from the given path. For more
|
|
/// details about paths, see the [Paths section]($syntax/#paths).
|
|
/// - Raw bytes from which the syntax should be decoded.
|
|
/// - An array where each item is one of the above.
|
|
///
|
|
/// ````example
|
|
/// #set raw(syntaxes: "SExpressions.sublime-syntax")
|
|
///
|
|
/// ```sexp
|
|
/// (defun factorial (x)
|
|
/// (if (zerop x)
|
|
/// ; with a comment
|
|
/// 1
|
|
/// (* x (factorial (- x 1)))))
|
|
/// ```
|
|
/// ````
|
|
#[parse(match args.named("syntaxes")? {
|
|
Some(sources) => Some(RawSyntax::load(engine.world, sources)?),
|
|
None => None,
|
|
})]
|
|
#[fold]
|
|
pub syntaxes: Derived<OneOrMultiple<DataSource>, Vec<RawSyntax>>,
|
|
|
|
/// The theme to use for syntax highlighting. Themes should be in the
|
|
/// [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html).
|
|
///
|
|
/// You can pass any of the following values:
|
|
///
|
|
/// - `{none}`: Disables syntax highlighting.
|
|
/// - `{auto}`: Highlights with Typst's default theme.
|
|
/// - A path string to load a theme file from the given path. For more
|
|
/// details about paths, see the [Paths section]($syntax/#paths).
|
|
/// - Raw bytes from which the theme should be decoded.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// ````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(match args.named::<Spanned<Smart<Option<DataSource>>>>("theme")? {
|
|
Some(Spanned { v: Smart::Custom(Some(source)), span }) => Some(Smart::Custom(
|
|
Some(RawTheme::load(engine.world, Spanned::new(source, span))?)
|
|
)),
|
|
Some(Spanned { v: Smart::Custom(None), .. }) => Some(Smart::Custom(None)),
|
|
Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto),
|
|
None => None,
|
|
})]
|
|
pub theme: Smart<Option<Derived<DataSource, RawTheme>>>,
|
|
|
|
/// 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.lines = Some(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
|
|
.get_ref(styles)
|
|
.as_ref()
|
|
.map(|s| s.to_lowercase())
|
|
.or(Some("txt".into()));
|
|
|
|
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 syntaxes = LazyCell::new(|| elem.syntaxes.get_cloned(styles));
|
|
let theme: &synt::Theme = match elem.theme.get_ref(styles) {
|
|
Smart::Auto => &RAW_THEME,
|
|
Smart::Custom(Some(theme)) => theme.derived.get(),
|
|
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| {
|
|
// Prefer user-provided syntaxes over built-in ones.
|
|
syntaxes
|
|
.derived
|
|
.iter()
|
|
.map(|syntax| syntax.get())
|
|
.chain(std::iter::once(&*RAW_SYNTAXES))
|
|
.find_map(|set| {
|
|
set.find_syntax_by_token(&token).map(|syntax| (set, 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 ShowSet for Packed<RawElem> {
|
|
fn show_set(&self, styles: StyleChain) -> Styles {
|
|
let mut out = Styles::new();
|
|
out.set(TextElem::overhang, false);
|
|
out.set(TextElem::lang, Lang::ENGLISH);
|
|
out.set(TextElem::hyphenate, Smart::Custom(false));
|
|
out.set(TextElem::size, TextSize(Em::new(0.8).into()));
|
|
out.set(TextElem::font, FontList(vec![FontFamily::new("DejaVu Sans Mono")]));
|
|
out.set(TextElem::cjk_latin_spacing, Smart::Custom(None));
|
|
if self.block.get(styles) {
|
|
out.set(ParElem::justify, 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)]
|
|
#[allow(
|
|
clippy::derived_hash_with_manual_eq,
|
|
reason = "https://github.com/typst/typst/pull/6560#issuecomment-3045393640"
|
|
)]
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialEq for RawContent {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
match (self, other) {
|
|
(RawContent::Text(a), RawContent::Text(b)) => a == b,
|
|
(lines @ RawContent::Lines(_), RawContent::Text(text))
|
|
| (RawContent::Text(text), lines @ RawContent::Lines(_)) => {
|
|
*text == lines.get()
|
|
}
|
|
(RawContent::Lines(a), RawContent::Lines(b)) => Iterator::eq(
|
|
a.iter().map(|(line, _)| line),
|
|
b.iter().map(|(line, _)| line),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
cast! {
|
|
RawContent,
|
|
self => self.get().into_value(),
|
|
v: EcoString => Self::Text(v),
|
|
}
|
|
|
|
/// A loaded syntax.
|
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
pub struct RawSyntax(Arc<ManuallyHash<SyntaxSet>>);
|
|
|
|
impl RawSyntax {
|
|
/// Load syntaxes from sources.
|
|
fn load(
|
|
world: Tracked<dyn World + '_>,
|
|
sources: Spanned<OneOrMultiple<DataSource>>,
|
|
) -> SourceResult<Derived<OneOrMultiple<DataSource>, Vec<RawSyntax>>> {
|
|
let loaded = sources.load(world)?;
|
|
let list = loaded
|
|
.iter()
|
|
.map(|data| Self::decode(&data.data).within(data))
|
|
.collect::<SourceResult<_>>()?;
|
|
Ok(Derived::new(sources.v, list))
|
|
}
|
|
|
|
/// Decode a syntax from a loaded source.
|
|
#[comemo::memoize]
|
|
#[typst_macros::time(name = "load syntaxes")]
|
|
fn decode(bytes: &Bytes) -> LoadResult<RawSyntax> {
|
|
let str = bytes.as_str()?;
|
|
|
|
let syntax = SyntaxDefinition::load_from_str(str, false, None)
|
|
.map_err(format_syntax_error)?;
|
|
|
|
let mut builder = SyntaxSetBuilder::new();
|
|
builder.add(syntax);
|
|
|
|
Ok(RawSyntax(Arc::new(ManuallyHash::new(
|
|
builder.build(),
|
|
typst_utils::hash128(bytes),
|
|
))))
|
|
}
|
|
|
|
/// Return the underlying syntax set.
|
|
fn get(&self) -> &SyntaxSet {
|
|
self.0.as_ref()
|
|
}
|
|
}
|
|
|
|
fn format_syntax_error(error: ParseSyntaxError) -> LoadError {
|
|
let pos = syntax_error_pos(&error);
|
|
LoadError::new(pos, "failed to parse syntax", error)
|
|
}
|
|
|
|
fn syntax_error_pos(error: &ParseSyntaxError) -> ReportPos {
|
|
match error {
|
|
ParseSyntaxError::InvalidYaml(scan_error) => {
|
|
let m = scan_error.marker();
|
|
ReportPos::full(
|
|
m.index()..m.index(),
|
|
LineCol::one_based(m.line(), m.col() + 1),
|
|
)
|
|
}
|
|
_ => ReportPos::None,
|
|
}
|
|
}
|
|
|
|
/// A loaded syntect theme.
|
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
pub struct RawTheme(Arc<ManuallyHash<synt::Theme>>);
|
|
|
|
impl RawTheme {
|
|
/// Load a theme from a data source.
|
|
fn load(
|
|
world: Tracked<dyn World + '_>,
|
|
source: Spanned<DataSource>,
|
|
) -> SourceResult<Derived<DataSource, Self>> {
|
|
let loaded = source.load(world)?;
|
|
let theme = Self::decode(&loaded.data).within(&loaded)?;
|
|
Ok(Derived::new(source.v, theme))
|
|
}
|
|
|
|
/// Decode a theme from bytes.
|
|
#[comemo::memoize]
|
|
fn decode(bytes: &Bytes) -> LoadResult<RawTheme> {
|
|
let mut cursor = std::io::Cursor::new(bytes.as_slice());
|
|
let theme =
|
|
synt::ThemeSet::load_from_reader(&mut cursor).map_err(format_theme_error)?;
|
|
Ok(RawTheme(Arc::new(ManuallyHash::new(theme, typst_utils::hash128(bytes)))))
|
|
}
|
|
|
|
/// Get the underlying syntect theme.
|
|
pub fn get(&self) -> &synt::Theme {
|
|
self.0.as_ref()
|
|
}
|
|
}
|
|
|
|
fn format_theme_error(error: syntect::LoadingError) -> LoadError {
|
|
let pos = match &error {
|
|
syntect::LoadingError::ParseSyntax(err, _) => syntax_error_pos(err),
|
|
_ => ReportPos::None,
|
|
};
|
|
LoadError::new(pos, "failed to parse theme", error)
|
|
}
|
|
|
|
/// 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", Locatable, 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 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>,
|
|
}
|
|
|
|
// Shorthands 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>);
|
|
|
|
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 = styles.get(RawElem::tab_size);
|
|
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.set(TextElem::span_offset, span_offset);
|
|
}
|
|
|
|
if style.foreground != foreground {
|
|
body = body.set(TextElem::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().into_format::<u8, u8>().into_components();
|
|
synt::Color { r, g, b, a }
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
/// 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: LazyLock<syntect::parsing::SyntaxSet> =
|
|
LazyLock::new(two_face::syntax::extra_no_newlines);
|
|
|
|
/// The default theme used for syntax highlighting.
|
|
pub static RAW_THEME: LazyLock<synt::Theme> = LazyLock::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),
|
|
],
|
|
});
|