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:
Myriad-Dreamin 2024-07-24 13:41:54 +08:00 committed by GitHub
parent ab3c642038
commit ff72962334
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 164 additions and 30 deletions

5
Cargo.lock generated
View file

@ -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",
]

View file

@ -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]

View file

@ -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> {

View file

@ -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: &regex::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>"###);
}

View 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>"###);
}

View 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,
}
}
}