mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
feat(typlite): add equation support (#454)
* feat(typlite): add equation support * feat(typlite): minimize snapshot * dev(typlite): eliminate unnecessary spacing * dev: redact totally * fix: snapshot
This commit is contained in:
parent
ab3c642038
commit
ff72962334
6 changed files with 164 additions and 30 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -4129,9 +4129,14 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
|||
name = "typlite"
|
||||
version = "0.11.16"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"comemo 0.4.0",
|
||||
"ecow 0.2.2",
|
||||
"insta",
|
||||
"regex",
|
||||
"typst",
|
||||
"typst-assets",
|
||||
"typst-svg",
|
||||
"typst-syntax 0.11.1",
|
||||
]
|
||||
|
||||
|
|
|
@ -15,8 +15,14 @@ typst-syntax.workspace = true
|
|||
ecow.workspace = true
|
||||
comemo.workspace = true
|
||||
|
||||
typst.workspace = true
|
||||
typst-svg.workspace = true
|
||||
base64.workspace = true
|
||||
typst-assets = { workspace = true, features = ["fonts"] }
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
regex.workspace = true
|
||||
|
||||
[features]
|
||||
|
||||
|
|
|
@ -3,9 +3,13 @@
|
|||
mod library;
|
||||
pub mod scopes;
|
||||
mod value;
|
||||
mod world;
|
||||
|
||||
use base64::Engine;
|
||||
use scopes::Scopes;
|
||||
use typst::{eval::Tracer, layout::Abs};
|
||||
use value::{Args, Value};
|
||||
use world::LiteWorld;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
@ -76,15 +80,17 @@ impl TypliteWorker {
|
|||
match node.kind() {
|
||||
RawLang | RawDelim | RawTrimmed => Err("converting clause")?,
|
||||
|
||||
Math | MathIdent | MathAlignPoint | MathDelimited | MathAttach | MathPrimes
|
||||
| MathFrac | MathRoot => Err("converting math node")?,
|
||||
|
||||
// Error nodes
|
||||
Error => Err(node.clone().into_text().to_string())?,
|
||||
Eof | None => Ok(Value::None),
|
||||
|
||||
// Non-leaf nodes
|
||||
Math => self.reduce(node),
|
||||
Markup => self.reduce(node),
|
||||
Code => self.reduce(node),
|
||||
|
||||
Equation => self.equation(node),
|
||||
CodeBlock => {
|
||||
let code_block: ast::CodeBlock = node.cast().unwrap();
|
||||
self.eval(code_block.body().to_untyped())
|
||||
|
@ -121,14 +127,6 @@ impl TypliteWorker {
|
|||
EnumMarker => Self::str(node),
|
||||
TermItem => self.term_item(node),
|
||||
TermMarker => Self::str(node),
|
||||
Equation => Self::equation(node),
|
||||
MathIdent => Self::str(node),
|
||||
MathAlignPoint => Self::str(node),
|
||||
MathDelimited => Self::str(node),
|
||||
MathAttach => Self::str(node),
|
||||
MathPrimes => Self::str(node),
|
||||
MathFrac => Self::str(node),
|
||||
MathRoot => Self::str(node),
|
||||
|
||||
// Punctuation
|
||||
// Hash => Self::char('#'),
|
||||
|
@ -252,6 +250,33 @@ impl TypliteWorker {
|
|||
Ok(Value::Content(s))
|
||||
}
|
||||
|
||||
fn render(&mut self, node: &SyntaxNode, inline: bool) -> Result<Value> {
|
||||
let color = "#c0caf5";
|
||||
|
||||
let main = Source::detached(eco_format!(
|
||||
r##"#set page(width: auto, height: auto, margin: (y: 0.45em, rest: 0em));#set text(rgb("{color}"))
|
||||
{}"##,
|
||||
node.clone().into_text()
|
||||
));
|
||||
let world = LiteWorld::new(main);
|
||||
let mut tracer = Tracer::default();
|
||||
let document = typst::compile(&world, &mut tracer)
|
||||
.map_err(|e| format!("compiling math node: {e:?}"))?;
|
||||
|
||||
let svg_payload = typst_svg::svg_merged(&document, Abs::zero());
|
||||
let base64 = base64::engine::general_purpose::STANDARD.encode(svg_payload);
|
||||
|
||||
if inline {
|
||||
Ok(Value::Content(eco_format!(
|
||||
r#"<img style="vertical-align: -0.35em" src="data:image/svg+xml;base64,{base64}" alt="typst-block" />"#
|
||||
)))
|
||||
} else {
|
||||
Ok(Value::Content(eco_format!(
|
||||
r#"<p align="center"><img src="data:image/svg+xml;base64,{base64}" alt="typst-block" /></p>"#
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn char(arg: char) -> Result<Value> {
|
||||
Ok(Value::Content(arg.into()))
|
||||
}
|
||||
|
@ -379,19 +404,10 @@ impl TypliteWorker {
|
|||
}
|
||||
|
||||
#[cfg(not(feature = "texmath"))]
|
||||
fn equation(node: &SyntaxNode) -> Result<Value> {
|
||||
let equation = node.cast::<ast::Equation>().unwrap();
|
||||
let mut s = EcoString::new();
|
||||
fn equation(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||
let equation: ast::Equation = node.cast().unwrap();
|
||||
|
||||
#[rustfmt::skip]
|
||||
s.push_str(if equation.block() { "```typ\n$\n" } else { "`$" });
|
||||
for e in equation.body().exprs() {
|
||||
Self::str(e.to_untyped())?;
|
||||
}
|
||||
#[rustfmt::skip]
|
||||
s.push_str(if equation.block() { "\n$\n```\n" } else { "$`" });
|
||||
|
||||
Ok(Value::Content(s))
|
||||
self.render(node, !equation.block())
|
||||
}
|
||||
|
||||
fn let_binding(&self, node: &SyntaxNode) -> Result<Value> {
|
||||
|
|
|
@ -1,9 +1,26 @@
|
|||
mod model;
|
||||
mod rendering;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn conv(s: &str) -> EcoString {
|
||||
Typlite::new_with_content(s.trim()).convert().unwrap()
|
||||
let res = Typlite::new_with_content(s.trim()).convert().unwrap();
|
||||
static REG: OnceLock<Regex> = OnceLock::new();
|
||||
let reg = REG.get_or_init(|| Regex::new(r#"data:image/svg\+xml;base64,([^"]+)"#).unwrap());
|
||||
let res = reg.replace(&res, |_captures: ®ex::Captures| {
|
||||
// let hash = _captures.get(1).unwrap().as_str();
|
||||
// format!(
|
||||
// "data:image-hash/svg+xml;base64,siphash128:{:x}",
|
||||
// typst::util::hash128(hash)
|
||||
// )
|
||||
"data:image-hash/svg+xml;base64,redacted"
|
||||
});
|
||||
|
||||
res.into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -44,11 +61,5 @@ Some inlined raw `a`, ```c b```
|
|||
$
|
||||
1/2 + 1/3 = 5/6
|
||||
$
|
||||
"###), @r###"
|
||||
```typ
|
||||
$
|
||||
|
||||
$
|
||||
```
|
||||
"###);
|
||||
"###), @r###"<p align="center"><img src="data:image-hash/svg+xml;base64,redacted" alt="typst-block" /></p>"###);
|
||||
}
|
||||
|
|
11
crates/typlite/src/tests/rendering.rs
Normal file
11
crates/typlite/src/tests/rendering.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use crate::tests::*;
|
||||
|
||||
#[test]
|
||||
fn test_math_equation() {
|
||||
insta::assert_snapshot!(conv(r###"
|
||||
$integral x dif x$
|
||||
"###), @r###"<img style="vertical-align: -0.35em" src="data:image-hash/svg+xml;base64,redacted" alt="typst-block" />"###);
|
||||
insta::assert_snapshot!(conv(r###"
|
||||
$ integral x dif x $
|
||||
"###), @r###"<p align="center"><img src="data:image-hash/svg+xml;base64,redacted" alt="typst-block" /></p>"###);
|
||||
}
|
85
crates/typlite/src/world.rs
Normal file
85
crates/typlite/src/world.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use comemo::Prehashed;
|
||||
use typst::{
|
||||
diag::{FileError, FileResult},
|
||||
foundations::{Bytes, Datetime},
|
||||
text::{Font, FontBook},
|
||||
Library, World,
|
||||
};
|
||||
use typst_syntax::{FileId, Source};
|
||||
|
||||
/// A world for TypstLite.
|
||||
pub struct LiteWorld {
|
||||
main: Source,
|
||||
base: &'static LiteBase,
|
||||
}
|
||||
|
||||
impl LiteWorld {
|
||||
/// Create a new world for a single test.
|
||||
///
|
||||
/// This is cheap because the shared base for all test runs is lazily
|
||||
/// initialized just once.
|
||||
pub fn new(main: Source) -> Self {
|
||||
static BASE: OnceLock<LiteBase> = OnceLock::new();
|
||||
Self {
|
||||
main,
|
||||
base: BASE.get_or_init(LiteBase::default),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl World for LiteWorld {
|
||||
fn library(&self) -> &Prehashed<Library> {
|
||||
&self.base.library
|
||||
}
|
||||
|
||||
fn book(&self) -> &Prehashed<FontBook> {
|
||||
&self.base.book
|
||||
}
|
||||
|
||||
fn main(&self) -> Source {
|
||||
self.main.clone()
|
||||
}
|
||||
|
||||
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||
if id == self.main.id() {
|
||||
Ok(self.main.clone())
|
||||
} else {
|
||||
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
|
||||
}
|
||||
|
||||
fn font(&self, index: usize) -> Option<Font> {
|
||||
Some(self.base.fonts[index].clone())
|
||||
}
|
||||
|
||||
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared foundation of all lite worlds.
|
||||
struct LiteBase {
|
||||
library: Prehashed<Library>,
|
||||
book: Prehashed<FontBook>,
|
||||
fonts: Vec<Font>,
|
||||
}
|
||||
|
||||
impl Default for LiteBase {
|
||||
fn default() -> Self {
|
||||
let fonts: Vec<_> = typst_assets::fonts()
|
||||
.flat_map(|data| Font::iter(Bytes::from_static(data)))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
library: Prehashed::new(typst::Library::default()),
|
||||
book: Prehashed::new(FontBook::from_fonts(&fonts)),
|
||||
fonts,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue