refactor: remove unused code (#1728)

This commit is contained in:
Myriad-Dreamin 2025-05-01 17:58:15 +08:00 committed by GitHub
parent 6b7ca47f23
commit d85bd7428a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 22 additions and 1598 deletions

View file

@ -1,10 +1,8 @@
use std::path::Path;
use std::sync::{Arc, LazyLock};
use std::sync::Arc;
use ecow::{eco_format, EcoString};
use tinymist_world::{EntryReader, ShadowApi, TaskInputs};
use typlite::scopes::Scopes;
use typlite::value::Value;
use typlite::TypliteFeat;
use typst::diag::StrResult;
use typst::foundations::Bytes;
@ -13,9 +11,6 @@ use typst::World;
use crate::analysis::SharedContext;
pub(crate) fn convert_docs(ctx: &SharedContext, content: &str) -> StrResult<EcoString> {
static DOCS_LIB: LazyLock<Arc<Scopes<Value>>> =
LazyLock::new(|| Arc::new(typlite::library::docstring_lib()));
let entry = ctx.world.entry_state();
let entry = entry.select_in_workspace(Path::new("__tinymist_docs__.typ"));
@ -28,7 +23,6 @@ pub(crate) fn convert_docs(ctx: &SharedContext, content: &str) -> StrResult<EcoS
w.take_db();
let conv = typlite::Typlite::new(Arc::new(w))
.with_library(DOCS_LIB.clone())
.with_feature(TypliteFeat {
color_theme: Some(ctx.analysis.color_theme),
annotate_elem: true,

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/base.typ
---
siphash128_13:f242a739ddf7cdce8041455cd09bf221

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/enum.typ
---
siphash128_13:120c2e9245d767d648fd52a8564c9efc

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/enum2.typ
---
siphash128_13:120c2e9245d767d648fd52a8564c9efc

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/figure_caption.typ
---
siphash128_13:17d544d88231b74b1119c35f627026b

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/figure_image.typ
---
siphash128_13:89ee713812f00bde9ac174f72c81760

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/figure_image_alt.typ
---
siphash128_13:89ee713812f00bde9ac174f72c81760

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/image.typ
---
siphash128_13:89ee713812f00bde9ac174f72c81760

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/image_alt.typ
---
siphash128_13:89ee713812f00bde9ac174f72c81760

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/link.typ
---
siphash128_13:35e614ded7c81c7fb6781d77872add56

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/link2.typ
---
siphash128_13:2374bfc8248e276ed1549f5d6a8b4a40

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/link3.typ
---
siphash128_13:5d5f436195b9b0b0f206881bc4d810f8

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/list.typ
---
siphash128_13:dd68d2d40ddf137ad77719e71c56a19e

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/math_block.typ
---
siphash128_13:ca4f0e6c5b2afee90d9736cb2d3bd6ba

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/math_block2.typ
---
siphash128_13:1c9f3489f7742ef572998ff2b4fd5abd

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/math_inline.typ
---
siphash128_13:2ac3d241b41c4ee23a122b73e43c8063

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/outline.typ
---
siphash128_13:549cf83e9b77d8ae061c95ceb4f93ef6

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/raw_inline.typ
---
siphash128_13:fe468826fde99ac8a0e77767d4045199

View file

@ -1,6 +0,0 @@
---
source: crates/typlite/src/tests.rs
expression: hash
input_file: crates/typlite/src/fixtures/integration/table.typ
---
siphash128_13:ce1b6f668016a12edf304ab7f38aea42

View file

@ -5,10 +5,7 @@ pub mod common;
mod error;
pub mod library;
pub mod parser;
pub mod scopes;
pub mod tags;
pub mod value;
pub mod worker;
pub mod writer;
use std::path::PathBuf;
@ -136,8 +133,6 @@ pub struct TypliteFeat {
pub struct Typlite {
/// The universe to use for the conversion.
world: Arc<LspWorld>,
/// library to use for the conversion.
library: Option<Arc<scopes::Scopes<value::Value>>>,
/// Features for the conversion.
feat: TypliteFeat,
/// The format to use for the conversion.
@ -152,18 +147,11 @@ impl Typlite {
pub fn new(world: Arc<LspWorld>) -> Self {
Self {
world,
library: None,
feat: Default::default(),
format: Format::Md,
}
}
/// Set library to use for the conversion.
pub fn with_library(mut self, library: Arc<scopes::Scopes<value::Value>>) -> Self {
self.library = Some(library);
self
}
/// Set conversion feature
pub fn with_feature(mut self, feat: TypliteFeat) -> Self {
self.feat = feat;

View file

@ -1,258 +1,18 @@
//! # Typlite Library
use crate::{scopes::Scopes, tinymist_std::typst::diag::EcoString, worker::TypliteWorker};
use super::*;
use ecow::eco_format;
use typst_syntax::{ast, SyntaxKind, SyntaxNode};
use value::*;
mod docstring;
pub use docstring::docstring_lib;
pub fn library() -> Scopes<Value> {
let mut scopes = Scopes::new();
scopes.define("link", link as RawFunc);
scopes.define("kbd", kbd as RawFunc);
scopes.define("md-alter", md_alter as RawFunc);
scopes.define("image", image as RawFunc);
scopes.define("figure", figure as RawFunc);
scopes.define("raw", raw as RawFunc);
scopes.define("strike", strike as RawFunc);
scopes.define("pad", pad as RawFunc);
scopes.define("note-box", note as RawFunc);
scopes.define("tip-box", tip as RawFunc);
scopes.define("important-box", important_box as RawFunc);
scopes.define("warning-box", warning_box as RawFunc);
scopes.define("caution-box", caution_box as RawFunc);
scopes.define("table", table as RawFunc);
scopes.define("grid", grid as RawFunc);
scopes
}
/// Evaluates a link.
pub fn link(mut args: Args) -> Result<Value> {
let dest = get_pos_named!(args, dest: EcoString);
let body = get_pos_named!(args, body: Content);
Ok(Value::Content(eco_format!("[{body}]({dest})")))
}
/// Evaluates an image.
pub fn image(mut args: Args) -> Result<Value> {
let path = get_pos_named!(args, path: EcoString);
let alt = get_named!(args, alt: EcoString := "");
Ok(Value::Image { path, alt })
}
/// Evaluates a figure.
pub fn figure(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, path: Value);
let caption = get_named!(args, caption: Option<Value>).map(TypliteWorker::value);
match (body, caption) {
(Value::Image { path, alt }, None) => Ok(Value::Content(eco_format!("![{alt}]({path})"))),
(Value::Image { path, alt }, Some(caption)) if args.vm.feat.gfm => Ok(Value::Content(
eco_format!("![{caption}, {alt}]({path} {caption:?})"),
)),
(Value::Image { path, alt }, Some(caption)) => {
Ok(Value::Content(eco_format!("![{caption}, {alt}]({path})")))
}
_ => Err("figure only accepts image as body".into()),
}
}
/// Evaluates a raw.
pub fn raw(mut args: Args) -> Result<Value> {
let content = get_pos_named!(args, content: EcoString);
let max_consecutive_backticks = content
.chars()
.fold((0, 0), |(max, count), c| {
if c == '`' {
(max, count + 1)
} else {
(max.max(count), 0)
}
})
.0;
Ok(Value::Content(eco_format!(
"{backticks}\n{content}\n{backticks}",
backticks = "`".repeat((max_consecutive_backticks + 1).max(3)),
)))
}
/// Evaluates a strike.
pub fn strike(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, body: Content);
Ok(Value::Content(eco_format!("~~{body}~~")))
}
/// Evaluates a padded content.
pub fn pad(mut args: Args) -> Result<Value> {
Ok(get_pos_named!(args, path: Value))
}
/// Evaluates a `kbd` element.
pub fn kbd(mut args: Args) -> Result<Value> {
let key = get_pos_named!(args, key: EcoString);
Ok(Value::Content(eco_format!("<kbd>{key}</kbd>")))
}
/// Evaluates a markdown alteration.
pub fn md_alter(mut args: Args) -> Result<Value> {
let _: () = get_pos_named!(args, left: ());
let right = get_pos_named!(args, right: LazyContent);
Ok(Value::Content(right.0))
}
/// Evaluates a note.
pub fn note(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, body: Content);
Ok(note_box("NOTE", body))
}
/// Evaluates a tip note box.
pub fn tip(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, body: Content);
Ok(note_box("TIP", body))
}
/// Creates a important note box.
pub fn important_box(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, body: Content);
Ok(note_box("IMPORTANT", body))
}
/// Creates a warning note box.
pub fn warning_box(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, body: Content);
Ok(note_box("WARNING", body))
}
/// Creates a caution note box.
pub fn caution_box(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, body: Content);
Ok(note_box("CAUTION", body))
}
fn note_box(title: &str, body: Content) -> Value {
let mut res = EcoString::new();
res.push_str("> [!");
res.push_str(title);
res.push_str("]\n");
let body = body.0;
for line in body.lines() {
res.push_str("> ");
res.push_str(line);
res.push('\n');
}
Value::Content(res)
}
/// Evaluates a table.
pub fn table(args: Args) -> Result<Value> {
table_eval(args, &EcoString::from("table"))
}
/// Evaluates a grid.
pub fn grid(args: Args) -> Result<Value> {
table_eval(args, &EcoString::from("grid"))
}
fn table_eval(mut args: Args, kind: &EcoString) -> Result<Value> {
let columns = if let Some(columns) = args.get_named_("columns") {
match columns.kind() {
SyntaxKind::Array => {
let array: ast::Array = args.get_named_("columns").unwrap().cast().unwrap();
array.items().count()
}
SyntaxKind::Int => {
let int_val: ast::Int = args.get_named_("columns").unwrap().cast().unwrap();
int_val.get().try_into().unwrap()
}
other => return Err(format!("invalid columns argument of type {:?}", other).into()),
}
} else {
1
};
let header_field = SyntaxNode::inner(
SyntaxKind::FieldAccess,
vec![
SyntaxNode::leaf(SyntaxKind::Ident, kind),
SyntaxNode::leaf(SyntaxKind::Dot, "."),
SyntaxNode::leaf(SyntaxKind::Ident, "header"),
],
);
let footer_field = SyntaxNode::inner(
SyntaxKind::FieldAccess,
vec![
SyntaxNode::leaf(SyntaxKind::Ident, kind),
SyntaxNode::leaf(SyntaxKind::Dot, "."),
SyntaxNode::leaf(SyntaxKind::Ident, "footer"),
],
);
let mut header: Vec<EcoString> = Vec::new();
let mut cells: Vec<EcoString> = Vec::new();
while let Some(pos_arg) = args.pos.pop() {
if pos_arg.kind() != SyntaxKind::FuncCall {
let evaluated = args.vm.eval(pos_arg)?;
cells.push(TypliteWorker::value(evaluated));
} else {
let func_call: ast::FuncCall = pos_arg.cast().unwrap();
let first_child = pos_arg.children().next().unwrap();
if header_field.spanless_eq(first_child) {
let mut header_args = Args::new(args.vm, func_call.args());
while let Some(arg) = header_args.pos.pop() {
let evaluated = header_args.vm.eval(arg)?;
header.push(TypliteWorker::value(evaluated));
}
} else {
let evaluated = args.vm.eval(pos_arg)?;
cells.push(TypliteWorker::value(evaluated));
}
if footer_field.spanless_eq(first_child) {
let mut footer_args = Args::new(args.vm, func_call.args());
while let Some(arg) = footer_args.pos.pop() {
let evaluated = footer_args.vm.eval(arg)?;
cells.push(TypliteWorker::value(evaluated));
}
}
}
}
let mut res = EcoString::from("<table>\n");
if !header.is_empty() {
res.push_str(" <thead>\n <tr>\n");
for cell in &header {
res.push_str(&eco_format!(" <th>{}</th>\n", cell));
}
res.push_str(" </tr>\n </thead>\n");
}
res.push_str(" <tbody>\n");
for row in cells.chunks(columns) {
res.push_str(" <tr>\n");
for cell in row {
res.push_str(&eco_format!(" <td>{}</td>\n", cell));
}
res.push_str(" </tr>\n");
}
res.push_str(" </tbody>\n</table>");
Ok(Value::Content(res))
}
// scopes.define("link", link as RawFunc);
// scopes.define("kbd", kbd as RawFunc);
// scopes.define("md-alter", md_alter as RawFunc);
// scopes.define("image", image as RawFunc);
// scopes.define("figure", figure as RawFunc);
// scopes.define("raw", raw as RawFunc);
// scopes.define("strike", strike as RawFunc);
// scopes.define("pad", pad as RawFunc);
// scopes.define("note-box", note as RawFunc);
// scopes.define("tip-box", tip as RawFunc);
// scopes.define("important-box", important_box as RawFunc);
// scopes.define("warning-box", warning_box as RawFunc);
// scopes.define("caution-box", caution_box as RawFunc);
// scopes.define("table", table as RawFunc);
// scopes.define("grid", grid as RawFunc);
// pub fn cross_link(mut args: Args) -> typlite::Result<Value>

View file

@ -1,59 +0,0 @@
use super::*;
pub fn docstring_lib() -> Scopes<Value> {
let mut scopes = library();
scopes.define("example", example as RawFunc);
scopes
}
/// Evaluate a `example`.
pub fn example(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, body: &SyntaxNode);
let body = body
.cast::<ast::Raw>()
.ok_or_else(|| format!("expected raw, found {:?}", body.kind()))?;
let lang = body.lang().map(|l| l.get().as_str()).unwrap_or("typ");
// Handle example docs specially.
// <https://github.com/typst/typst/blob/070e3144b33e9a9e9839c138df2b0a13dde7abc7/docs/src/html.rs#L355>
let mut display = String::new();
let mut compile = String::new();
for line in body.lines() {
let line = line.get();
if let Some(suffix) = line.strip_prefix(">>>") {
compile.push_str(suffix);
compile.push('\n');
} else if let Some(suffix) = line.strip_prefix("<<< ") {
display.push_str(suffix);
display.push('\n');
} else {
display.push_str(line);
display.push('\n');
compile.push_str(line);
compile.push('\n');
}
}
let mut s = EcoString::new();
s.push_str("```");
s.push_str(lang);
s.push('\n');
s.push_str(&display);
s.push('\n');
s.push_str("```");
s.push('\n');
if !args.vm.feat.remove_html {
let is_code = lang == "typc";
let rendered =
args.vm
.render_code("", &compile, !is_code, "left", r#"width="500px""#, false)?;
s.push_str(&TypliteWorker::value(rendered));
}
Ok(Value::Content(s))
}

View file

@ -7,9 +7,8 @@ use std::{
};
use clap::Parser;
use ecow::{eco_format, EcoString};
use tinymist_project::WorldProvider;
use typlite::{common::Format, value::*, TypliteFeat};
use typlite::{common::Format, TypliteFeat};
use typlite::{CompileOnceArgs, Typlite};
/// Common arguments of compile, watch, and query.
@ -62,12 +61,10 @@ fn main() -> typlite::Result<()> {
let universe = args.compile.resolve().map_err(|err| format!("{err:?}"))?;
let world = universe.snapshot();
let converter = Typlite::new(Arc::new(world))
.with_library(lib())
.with_feature(TypliteFeat {
assets_path: assets_path.clone(),
..Default::default()
});
let converter = Typlite::new(Arc::new(world)).with_feature(TypliteFeat {
assets_path: assets_path.clone(),
..Default::default()
});
let doc = match converter.convert_doc() {
Ok(doc) => doc,
Err(err) => return Err(format!("failed to convert document: {err}").into()),
@ -168,31 +165,3 @@ fn main() -> typlite::Result<()> {
Ok(())
}
fn lib() -> Arc<typlite::scopes::Scopes<Value>> {
let mut scopes = typlite::library::docstring_lib();
// todo: how to import this function correctly?
scopes.define("cross-link", cross_link as RawFunc);
Arc::new(scopes)
}
/// Evaluate a `cross-link`.
pub fn cross_link(mut args: Args) -> typlite::Result<Value> {
let dest = get_pos_named!(args, dest: EcoString);
let body = get_pos_named!(args, body: Content);
let dest = std::path::Path::new(dest.as_str()).with_extension("html");
let mut dest = dest.as_path();
// strip leading `/` from the path
if let Ok(s) = dest.strip_prefix("/") {
dest = s;
}
Ok(Value::Content(eco_format!(
"[{body}](https://myriad-dreamin.github.io/tinymist/{dest})",
dest = dest.to_string_lossy()
)))
}

View file

@ -1,105 +0,0 @@
//! Variable scopes.
use super::*;
use std::collections::HashMap;
/// A single scope.
#[derive(Debug, Clone)]
pub struct Scope<T> {
map: HashMap<String, T>,
}
impl<T> Default for Scope<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Scope<T> {
/// Create a new, empty scope.
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}
/// Define a variable in this scope.
pub fn define(&mut self, name: String, val: T) {
self.map.insert(name, val);
}
/// Try to access a variable immutably.
pub fn get(&self, var: &str) -> Option<&T> {
self.map.get(var)
}
/// Try to access a variable mutably.
pub fn get_mut(&mut self, var: &str) -> Option<&mut T> {
self.map.get_mut(var)
}
}
/// A stack of scopes.
#[derive(Debug, Default, Clone)]
pub struct Scopes<T> {
/// The active scope.
pub top: Scope<T>,
/// The stack of lower scopes.
pub scopes: Vec<Scope<T>>,
}
impl<T> Scopes<T> {
/// Create a new, empty hierarchy of scopes.
pub fn new() -> Self {
Self {
top: Scope::new(),
scopes: vec![],
}
}
/// Enter a new scope.
pub fn enter(&mut self) {
self.scopes.push(std::mem::take(&mut self.top));
}
/// Exit the topmost scope.
///
/// This panics if no scope was entered.
pub fn exit(&mut self) {
self.top = self.scopes.pop().expect("no pushed scope");
}
/// Try to access a variable immutably.
pub fn get(&self, var: &str) -> Result<&T> {
std::iter::once(&self.top)
.chain(self.scopes.iter().rev())
.find_map(|scope| scope.get(var))
.ok_or_else(|| unknown_variable(var))
}
/// Try to access a variable immutably in math.
pub fn get_in_math(&self, var: &str) -> Result<&T> {
std::iter::once(&self.top)
.chain(self.scopes.iter().rev())
.find_map(|scope| scope.get(var))
.ok_or_else(|| unknown_variable(var))
}
/// Try to access a variable mutably.
pub fn get_mut(&mut self, var: &str) -> Result<&mut T> {
std::iter::once(&mut self.top)
.chain(&mut self.scopes.iter_mut().rev())
.find_map(|scope| scope.get_mut(var))
.ok_or_else(|| unknown_variable(var))
}
/// Define a variable in the current scope.
pub fn define(&mut self, arg: &str, v: impl Into<T>) {
self.top.define(arg.to_string(), v.into());
}
}
/// The error message when a variable is not found.
fn unknown_variable(var: &str) -> Error {
format!("unknown variable: {var}").into()
}

View file

@ -1,191 +0,0 @@
//! # Typlite Values
use crate::tinymist_std::typst::diag::EcoString;
use crate::worker::TypliteWorker;
use core::fmt;
use typst_syntax::{
ast::{self, AstNode},
SyntaxNode,
};
use crate::*;
pub type RawFunc = fn(Args) -> Result<Value>;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Value {
None,
RawFunc(RawFunc),
Str(EcoString),
Content(EcoString),
Image { path: EcoString, alt: EcoString },
}
impl From<RawFunc> for Value {
fn from(func: RawFunc) -> Self {
Self::RawFunc(func)
}
}
pub struct Content(pub EcoString);
impl fmt::Display for Content {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
pub struct LazyContent(pub EcoString);
impl fmt::Display for LazyContent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
pub struct Args<'a> {
pub vm: &'a mut TypliteWorker,
pub args: ast::Args<'a>,
pub pos: Vec<&'a SyntaxNode>,
}
impl<'a> Args<'a> {
pub fn new(worker: &'a mut TypliteWorker, args: ast::Args<'a>) -> Self {
let pos = args
.items()
.filter_map(|item| match item {
ast::Arg::Pos(pos) => Some(pos.to_untyped()),
_ => None,
})
.rev()
.collect();
Self {
vm: worker,
args,
pos,
}
}
pub fn get_named_(&mut self, key: &str) -> Option<&'a SyntaxNode> {
// find named
for item in self.args.items() {
if let ast::Arg::Named(named) = item {
if named.name().get() == key {
return Some(named.expr().to_untyped());
}
}
}
None
}
pub fn get(&mut self, key: &str) -> Result<&'a SyntaxNode> {
if let Some(named) = self.get_named_(key) {
return Ok(named);
}
// find positional
Ok(self
.pos
.pop()
.ok_or_else(|| format!("missing positional arguments: {key}"))?)
}
pub fn parse<T: Eval<'a>>(&mut self, node: &'a SyntaxNode) -> Result<T> {
T::eval(node, self.vm)
}
}
#[macro_export]
macro_rules! get_pos_named {
(
$args:expr,
$key:ident: $ty:ty
) => {{
let raw = $args.get(stringify!($key))?;
$args.parse::<$ty>(raw)?
}};
}
pub use get_pos_named;
#[macro_export]
macro_rules! get_named {
(
$args:expr,
$key:ident: Option<$ty:ty>
) => {{
if let Some(raw) = $args.get_named_(stringify!($key)) {
Some($args.parse::<$ty>(raw)?)
} else {
None
}
}};
(
$args:expr,
$key:ident: $ty:ty
) => {{
let raw = $args.get_named(stringify!($key))?;
$args.parse::<$ty>(raw)?
}};
(
$args:expr,
$key:ident: $ty:ty := $default:expr
) => {{
if let Some(raw) = $args.get_named_(stringify!($key)) {
$args.parse::<$ty>(raw)?
} else {
$default.into()
}
}};
}
pub use get_named;
/// Evaluate an expression.
pub trait Eval<'a>: Sized {
/// Evaluate the expression to the output value.
fn eval(node: &'a SyntaxNode, vm: &mut TypliteWorker) -> Result<Self>;
}
impl<'a> Eval<'a> for () {
fn eval(_node: &'a SyntaxNode, _vm: &mut TypliteWorker) -> Result<Self> {
Ok(())
}
}
impl<'a> Eval<'a> for &'a SyntaxNode {
fn eval(node: &'a SyntaxNode, _vm: &mut TypliteWorker) -> Result<Self> {
Ok(node)
}
}
impl<'a> Eval<'a> for EcoString {
fn eval(node: &'a SyntaxNode, _vm: &mut TypliteWorker) -> Result<Self> {
let node: ast::Str = node
.cast()
.ok_or_else(|| format!("expected string, found {:?}", node.kind()))?;
Ok(node.get())
}
}
impl<'a> Eval<'a> for Value {
fn eval(node: &'a SyntaxNode, vm: &mut TypliteWorker) -> Result<Self> {
vm.eval(node)
}
}
impl<'a> Eval<'a> for Content {
fn eval(node: &'a SyntaxNode, vm: &mut TypliteWorker) -> Result<Self> {
Ok(Self(vm.convert(node)?))
}
}
impl<'a> Eval<'a> for LazyContent {
fn eval(node: &'a SyntaxNode, vm: &mut TypliteWorker) -> Result<Self> {
let node = match node.cast() {
Some(s @ ast::Closure { .. }) => s.body().to_untyped(),
None => node,
};
Ok(Self(vm.convert(node)?))
}
}

View file

@ -1,824 +0,0 @@
use std::fmt::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock};
use base64::Engine;
use ecow::{eco_format, EcoString};
use tinymist_analysis;
use tinymist_project::base::ShadowApi;
use tinymist_project::{EntryReader, LspWorld};
use typst::foundations::{Bytes, Dict, IntoValue};
use typst::layout::Abs;
use typst::syntax::{FileId, Source, SyntaxKind, SyntaxNode};
use typst::utils::LazyHash;
use typst::World;
use typst::WorldExt;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::ast::{Emph, Equation, Heading, Raw, Strong};
use crate::scopes::Scopes;
use crate::tinymist_std::path::unix_slash;
use crate::value::{Args, Value};
use crate::worker::SyntaxKind::Text;
use crate::Result;
use crate::TypliteFeat;
use crate::WorkspaceResolver;
/// Typlite worker for converting syntax nodes to markdown
#[derive(Clone)]
pub struct TypliteWorker {
pub current: FileId,
pub scopes: Arc<Scopes<Value>>,
pub world: Arc<LspWorld>,
pub list_depth: usize,
pub prepend_code: EcoString,
pub assets_numbering: usize,
/// Features for the conversion.
pub feat: TypliteFeat,
}
impl TypliteWorker {
/// Convert the content to a markdown string.
pub fn convert(&mut self, node: &SyntaxNode) -> Result<EcoString> {
Ok(Self::value(self.eval(node)?))
}
/// Eval the content
pub fn eval(&mut self, node: &SyntaxNode) -> Result<Value> {
use SyntaxKind::*;
let res = match node.kind() {
RawLang | RawDelim | RawTrimmed => Err("converting clause")?,
Math | MathIdent | MathAlignPoint | MathDelimited | MathAttach | MathPrimes
| MathFrac | MathRoot | MathShorthand | MathText => Err("converting math node")?,
// Error nodes
Error => Err(node.clone().into_text().to_string())?,
None | End => Ok(Value::None),
// Non-leaf nodes
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())
}
ContentBlock => {
let content_block: ast::ContentBlock = node.cast().unwrap();
self.eval(content_block.body().to_untyped())
}
Parenthesized => {
let parenthesized: ast::Parenthesized = node.cast().unwrap();
self.eval(parenthesized.expr().to_untyped())
}
// Text nodes
Text | Space | Parbreak => Self::str(node),
Linebreak => Self::char('\n'),
// Semantic nodes
Escape => Self::escape(node),
Shorthand => Self::shorthand(node),
SmartQuote => Self::str(node),
Strong => self.strong(node),
Emph => self.emph(node),
Raw => Self::raw(node),
Link => self.link(node),
Label => Self::label(node),
Ref => Self::label_ref(node),
RefMarker => Self::ref_marker(node),
Heading => self.heading(node),
HeadingMarker => Self::str(node),
ListItem => self.list_item(node),
ListMarker => Self::str(node),
EnumItem => self.enum_item(node),
EnumMarker => Self::str(node),
TermItem => self.term_item(node),
TermMarker => Self::str(node),
// Punctuation
// Hash => Self::char('#'),
Hash => Ok(Value::None),
LeftBrace => Self::char('{'),
RightBrace => Self::char('}'),
LeftBracket => Self::char('['),
RightBracket => Self::char(']'),
LeftParen => Self::char('('),
RightParen => Self::char(')'),
Comma => Self::char(','),
Semicolon => Ok(Value::None),
Colon => Self::char(':'),
Star => Self::char('*'),
Underscore => Self::char('_'),
Dollar => Self::char('$'),
Plus => Self::char('+'),
Minus => Self::char('-'),
Slash => Self::char('/'),
Hat => Self::char('^'),
Prime => Self::char('\''),
Dot => Self::char('.'),
Eq => Self::char('='),
Lt => Self::char('<'),
Gt => Self::char('>'),
// Compound punctuation
EqEq => Self::str(node),
ExclEq => Self::str(node),
LtEq => Self::str(node),
GtEq => Self::str(node),
PlusEq => Self::str(node),
HyphEq => Self::str(node),
StarEq => Self::str(node),
SlashEq => Self::str(node),
Dots => Self::str(node),
Arrow => Self::str(node),
Root => Self::str(node),
// Keywords
Auto => Self::str(node),
Not => Self::str(node),
And => Self::str(node),
Or => Self::str(node),
Let => Self::str(node),
Set => Self::str(node),
Show => Self::str(node),
Context => Self::str(node),
If => Self::str(node),
Else => Self::str(node),
For => Self::str(node),
In => Self::str(node),
While => Self::str(node),
Break => Self::str(node),
Continue => Self::str(node),
Return => Self::str(node),
Import => Self::str(node),
Include => Self::str(node),
As => Self::str(node),
LetBinding => self.let_binding(node),
FieldAccess => self.field_access(node),
FuncCall => self.func_call(node),
Contextual => self.contextual(node),
// Clause nodes
Named => Ok(Value::None),
Keyed => Ok(Value::None),
Unary => Ok(Value::None),
Binary => Ok(Value::None),
Spread => Ok(Value::None),
ImportItems => Ok(Value::None),
ImportItemPath => Ok(Value::None),
RenamedImportItem => Ok(Value::None),
Closure => Ok(Value::None),
Args => Ok(Value::None),
Params => Ok(Value::None),
// Ignored code expressions
Ident => Ok(Value::None),
Bool => Ok(Value::None),
Int => Ok(Value::None),
Float => Ok(Value::None),
Numeric => Ok(Value::None),
Str => Ok(Value::Str({
let s: ast::Str = node.cast().unwrap();
s.get()
})),
Array => Ok(Value::None),
Dict => Ok(Value::None),
// Ignored code expressions
SetRule => Ok(Value::None),
ShowRule => Ok(Value::None),
Destructuring => Ok(Value::None),
DestructAssignment => Ok(Value::None),
Conditional => Ok(Value::None),
WhileLoop => Ok(Value::None),
ForLoop => Ok(Value::None),
LoopBreak => Ok(Value::None),
LoopContinue => Ok(Value::None),
FuncReturn => Ok(Value::None),
ModuleImport => Ok(Value::None),
ModuleInclude => self.include(node),
// Ignored comments
LineComment => Ok(Value::None),
BlockComment => Ok(Value::None),
Shebang => Ok(Value::None),
};
if res.clone()? == Value::None
&& !matches!(
node.kind(),
Ident | Bool | Int | Float | Numeric | Str | Array | Dict
)
{
self.prepend_code += node.clone().into_text();
if node.kind() != Hash {
self.prepend_code += "\n"
};
}
res
}
fn reduce(&mut self, node: &SyntaxNode) -> Result<Value> {
let mut s = EcoString::new();
for child in node.children() {
s.push_str(&Self::value(self.eval(child)?));
}
Ok(Value::Content(s))
}
pub fn to_raw_block(&mut self, node: &SyntaxNode, inline: bool) -> Result<Value> {
let content = node.clone().into_text();
let s = if inline {
let mut s = EcoString::with_capacity(content.len() + 2);
s.push_str("`");
s.push_str(&content);
s.push_str("`");
s
} else {
let mut s = EcoString::with_capacity(content.len() + 15);
s.push_str("```");
let lang = match node.cast::<ast::Expr>() {
Some(ast::Expr::Text(..) | ast::Expr::Space(..)) => "typ",
Some(..) => "typc",
None => "typ",
};
s.push_str(lang);
s.push('\n');
s.push_str(&content);
s.push('\n');
s.push_str("```");
s
};
Ok(Value::Content(s))
}
pub fn render(
&mut self,
prepend_node: &SyntaxNode,
node: &SyntaxNode,
inline: bool,
) -> Result<Value> {
self.assets_numbering += 1;
let prepend_code = prepend_node.clone().into_text();
let code = node.clone().into_text();
// if let Some(assets_src_path) = &self.feat.assets_src_path {
// let file_name = assets_src_path
// .join(self.assets_numbering.to_string())
// .with_extension("typ");
// if let Err(e) = std::fs::write(&file_name, format!("#{{\n// render_code\n{}\n}}", code))
// {
// return Err(format!("failed to write code to file: {}", e).into());
// }
// }
self.render_code(&prepend_code, &code, false, "center", "", inline)
}
pub fn render_code(
&mut self,
prepend_code: &str,
code: &str,
is_markup: bool,
align: &str,
extra_attrs: &str,
inline: bool,
) -> Result<Value> {
let theme = self.feat.color_theme;
// let code_file_name = if let Some(assets_src_path) = &self.feat.assets_src_path {
// Some(
// assets_src_path
// .join(self.assets_numbering.to_string())
// .with_extension("typ"),
// )
// } else {
// None
// };
let code_file_name = None;
let mut render = |theme| self.render_inner(prepend_code, code, is_markup, theme);
let mut content = EcoString::new();
let inline_attrs = if inline {
r#" style="vertical-align: -0.35em""#
} else {
""
};
let write_error = |content: &mut EcoString, err: &str| {
let err = err.replace("`", r#"\`"#);
let _ = write!(content, "```\nRender Error\n{err}\n```");
};
let write_image = |content: &mut EcoString,
file_name: &std::path::Path,
code_file_name: Option<&PathBuf>,
inline_attrs: &str,
extra_attrs: &str| {
if let Some(code_file_name) = code_file_name {
let _ = write!(
content,
r#"<a href="{}"><img{inline_attrs} alt="typst-block" src="{}" {extra_attrs}/></a>"#,
code_file_name.display(),
file_name.display()
);
} else {
let _ = write!(
content,
r#"<img{inline_attrs} alt="typst-block" src="{}" {extra_attrs}/>"#,
file_name.display()
);
}
};
let write_picture = |content: &mut EcoString,
dark_file_name: &std::path::Path,
light_file_name: &std::path::Path,
code_file_name: Option<&PathBuf>,
inline_attrs: &str,
extra_attrs: &str| {
if let Some(code_file_name) = code_file_name {
let _ = write!(
content,
r#"<a href="{}"><picture><source media="(prefers-color-scheme: dark)" srcset="{}"><img{inline_attrs} alt="typst-block" src="{}" {extra_attrs}/></picture></a>"#,
code_file_name.display(),
dark_file_name.display(),
light_file_name.display()
);
} else {
let _ = write!(
content,
r#"<picture><source media="(prefers-color-scheme: dark)" srcset="{}"><img{inline_attrs} alt="typst-block" src="{}" {extra_attrs}/></picture>"#,
dark_file_name.display(),
light_file_name.display()
);
}
};
match theme {
Some(theme) => {
let data = match render(theme) {
Ok(data) => data,
Err(err) if self.feat.soft_error => {
write_error(&mut content, &err.to_string());
return Ok(Value::Content(content));
}
Err(err) => return Err(err),
};
if !inline {
let _ = write!(content, r#"<p align="{align}">"#);
}
if let Some(assets_path) = &self.feat.assets_path {
let file_name =
assets_path.join(format!("{}_{:?}.svg", self.assets_numbering, theme));
std::fs::write(&file_name, &data)
.map_err(|e| format!("failed to write SVG to file: {}", e))?;
write_image(
&mut content,
&file_name,
code_file_name.as_ref(),
inline_attrs,
extra_attrs,
);
} else {
let _ = write!(
content,
r#"<img{inline_attrs} alt="typst-block" src="data:image/svg+xml;base64,{data}" {extra_attrs}/>"#
);
}
if !inline {
content.push_str("</p>");
}
}
None => {
let dark = match render(crate::ColorTheme::Dark) {
Ok(d) => d,
Err(err) if self.feat.soft_error => {
write_error(&mut content, &err.to_string());
return Ok(Value::Content(content));
}
Err(err) => return Err(err),
};
let light = match render(crate::ColorTheme::Light) {
Ok(l) => l,
Err(err) if self.feat.soft_error => {
write_error(&mut content, &err.to_string());
return Ok(Value::Content(content));
}
Err(err) => return Err(err),
};
if !inline {
let _ = write!(content, r#"<p align="{align}">"#);
}
if let Some(assets_path) = &self.feat.assets_path {
let dark_file_name = assets_path.join(format!(
"{}_{:?}.svg",
self.assets_numbering,
crate::ColorTheme::Dark
));
let light_file_name = assets_path.join(format!(
"{}_{:?}.svg",
self.assets_numbering,
crate::ColorTheme::Light
));
write_picture(
&mut content,
&dark_file_name,
&light_file_name,
code_file_name.as_ref(),
inline_attrs,
extra_attrs,
);
} else {
let _ = write!(
content,
r#"<picture><source media="(prefers-color-scheme: dark)" srcset="data:image/svg+xml;base64,{dark}"><img{inline_attrs} alt="typst-block" src="data:image/svg+xml;base64,{light}" {extra_attrs}/></picture>"#
);
}
if !inline {
content.push_str("</p>");
}
}
}
Ok(Value::Content(content))
}
fn render_inner(
&mut self,
prepend_code: &str,
code: &str,
is_markup: bool,
theme: crate::ColorTheme,
) -> Result<String> {
static DARK_THEME_INPUT: LazyLock<Arc<LazyHash<Dict>>> = LazyLock::new(|| {
Arc::new(LazyHash::new(Dict::from_iter(std::iter::once((
"x-color-theme".into(),
"dark".into_value(),
)))))
});
let code = WrapCode(code, is_markup);
let inputs = match theme {
crate::ColorTheme::Dark => Some(DARK_THEME_INPUT.clone()),
crate::ColorTheme::Light => None,
};
let code = eco_format!(
r##"{prepend_code}
#set page(width: auto, height: auto, margin: (y: 0.45em, rest: 0em), fill: none);
#set text(fill: rgb("#c0caf5")) if sys.inputs.at("x-color-theme", default: none) == "dark";
{code}"##
);
let main = Bytes::new(code.as_bytes().to_owned());
let path = Path::new("__render__.typ");
let entry = self.world.entry_state().select_in_workspace(path);
let mut world = self.world.task(tinymist_project::TaskInputs {
entry: Some(entry),
inputs,
});
world.take_db();
world.map_shadow_by_id(world.main(), main).unwrap();
let document = typst::compile(&world).output;
let document = document.map_err(|diagnostics| {
let mut err = String::new();
let _ = write!(err, "compiling node: ");
let write_span = |span: typst_syntax::Span, err: &mut String| {
let file = span.id().map(|id| match id.package() {
Some(package) if WorkspaceResolver::is_package_file(id) => {
format!("{package}:{}", unix_slash(id.vpath().as_rooted_path()))
}
Some(_) | None => unix_slash(id.vpath().as_rooted_path()),
});
let range = world.range(span);
match (file, range) {
(Some(file), Some(range)) => {
let _ = write!(err, "{file:?}:{range:?}");
}
(Some(file), None) => {
let _ = write!(err, "{file:?}");
}
(None, Some(range)) => {
let _ = write!(err, "{range:?}");
}
_ => {
let _ = write!(err, "unknown location");
}
}
};
for s in diagnostics.iter() {
match s.severity {
typst::diag::Severity::Error => {
let _ = write!(err, "error: ");
}
typst::diag::Severity::Warning => {
let _ = write!(err, "warning: ");
}
}
err.push_str(&s.message);
err.push_str(" at ");
write_span(s.span, &mut err);
for hint in s.hints.iter() {
err.push_str("\nHint: ");
err.push_str(hint);
}
for trace in &s.trace {
write!(err, "\nTrace: {} at ", trace.v).unwrap();
write_span(trace.span, &mut err);
}
err.push('\n');
}
err
})?;
let svg_payload = typst_svg::svg_merged(&document, Abs::zero());
if let Some(assets_path) = &self.feat.assets_path {
let file_name = assets_path.join(format!("{}_{:?}.svg", self.assets_numbering, theme));
if let Err(e) = std::fs::write(&file_name, &svg_payload) {
return Err(format!("failed to write SVG to file: {}", e).into());
}
Ok(file_name.to_string_lossy().to_string())
} else {
Ok(base64::engine::general_purpose::STANDARD.encode(svg_payload))
}
}
fn char(arg: char) -> Result<Value> {
Ok(Value::Content(arg.into()))
}
fn str(node: &SyntaxNode) -> Result<Value> {
Ok(Value::Content(node.clone().into_text()))
}
pub fn value(res: Value) -> EcoString {
match res {
Value::None => EcoString::new(),
Value::Content(content) => content,
Value::Str(s) => s,
Value::Image { path, alt } => eco_format!("![{alt}]({path})"),
_ => eco_format!("{res:?}"),
}
}
fn escape(node: &SyntaxNode) -> Result<Value> {
// todo: escape characters
Self::str(node)
}
fn shorthand(node: &SyntaxNode) -> Result<Value> {
// todo: shorthands
Self::str(node)
}
fn strong(&mut self, node: &SyntaxNode) -> Result<Value> {
let mut s = EcoString::new();
let strong = node.cast::<Strong>().unwrap();
s.push_str("**");
s.push_str(&Self::value(self.eval(strong.body().to_untyped())?));
s.push_str("**");
Ok(Value::Content(s))
}
fn emph(&mut self, node: &SyntaxNode) -> Result<Value> {
let mut s = EcoString::new();
let emph = node.cast::<Emph>().unwrap();
s.push('_');
s.push_str(&Self::value(self.eval(emph.body().to_untyped())?));
s.push('_');
Ok(Value::Content(s))
}
fn heading(&mut self, node: &SyntaxNode) -> Result<Value> {
let mut s = EcoString::new();
let heading = node.cast::<Heading>().unwrap();
let level = heading.depth();
for _ in 0..level.get() {
s.push('#');
}
s.push(' ');
s.push_str(&Self::value(self.eval(heading.body().to_untyped())?));
Ok(Value::Content(s))
}
fn raw(node: &SyntaxNode) -> Result<Value> {
let mut s = EcoString::new();
let raw = node.cast::<Raw>().unwrap();
// Raw codes with typlite language will not be treated as a code block but
// directly output into the Markdown result.
if let Some(lang) = raw.lang() {
if &EcoString::from("typlite") == lang.get() {
for line in raw.lines() {
s.push_str(&Self::value(Self::str(line.to_untyped())?));
s.push('\n');
}
return Ok(Value::Content(s));
}
}
if raw.block() {
s.push_str(&Self::value(Self::str(node)?));
return Ok(Value::Content(s));
}
s.push('`');
for line in raw.lines() {
s.push_str(&Self::value(Self::str(line.to_untyped())?));
}
s.push('`');
Ok(Value::Content(s))
}
fn link(&mut self, node: &SyntaxNode) -> Result<Value> {
// GFM supports autolinks
if self.feat.gfm {
return Self::str(node);
}
let mut s = EcoString::new();
s.push('[');
s.push_str(&Self::value(Self::str(node)?));
s.push(']');
s.push('(');
s.push_str(&Self::value(Self::str(node)?));
s.push(')');
Ok(Value::Content(s))
}
fn label(_node: &SyntaxNode) -> Result<Value> {
Result::Ok(Value::None)
}
fn label_ref(node: &SyntaxNode) -> Result<Value> {
Self::str(node)
}
fn ref_marker(node: &SyntaxNode) -> Result<Value> {
Self::str(node)
}
fn list_item(&mut self, node: &SyntaxNode) -> Result<Value> {
let mut s = EcoString::new();
let list_item = node.cast::<ast::ListItem>().unwrap();
for _ in 0..self.list_depth {
s.push_str(" ");
}
s.push_str("- ");
if self.feat.annotate_elem {
let _ = write!(s, "<!-- typlite:begin:list-item {} -->", self.list_depth);
self.list_depth += 1;
}
s.push_str(&Self::value(self.eval(list_item.body().to_untyped())?));
if self.feat.annotate_elem {
self.list_depth -= 1;
let _ = write!(s, "<!-- typlite:end:list-item {} -->", self.list_depth);
}
Ok(Value::Content(s))
}
fn enum_item(&mut self, node: &SyntaxNode) -> Result<Value> {
let enum_item = node.cast::<ast::EnumItem>().unwrap();
let mut s = EcoString::new();
for _ in 0..self.list_depth {
s.push_str(" ");
}
if self.feat.annotate_elem {
let _ = write!(s, "<!-- typlite:begin:enum-item {} -->", self.list_depth);
self.list_depth += 1;
}
if let Some(num) = enum_item.number() {
s.push_str(&format!("{}. ", num));
} else {
s.push_str("1. ");
}
s.push_str(&Self::value(self.eval(enum_item.body().to_untyped())?));
if self.feat.annotate_elem {
self.list_depth -= 1;
let _ = write!(s, "<!-- typlite:end:enum-item {} -->", self.list_depth);
}
Ok(Value::Content(s))
}
fn term_item(&mut self, node: &SyntaxNode) -> Result<Value> {
self.reduce(node)
}
fn equation(&mut self, node: &SyntaxNode) -> Result<Value> {
let equation: Equation = node.cast().unwrap();
if self.feat.remove_html {
return self.to_raw_block(node, !equation.block());
}
self.render(&SyntaxNode::leaf(Text, ""), node, !equation.block())
}
fn let_binding(&self, node: &SyntaxNode) -> Result<Value> {
let _ = node;
Ok(Value::None)
}
fn field_access(&self, node: &SyntaxNode) -> Result<Value> {
let _ = node;
Ok(Value::None)
}
fn func_call(&mut self, node: &SyntaxNode) -> Result<Value> {
let c: ast::FuncCall = node.cast().unwrap();
let callee = match c.callee() {
ast::Expr::Ident(callee) => self.scopes.get(callee.get()),
ast::Expr::FieldAccess(..) => return Ok(Value::None),
_ => return Ok(Value::None),
}?;
let Value::RawFunc(func) = callee else {
return Err("callee is not a function")?;
};
func(Args::new(self, c.args()))
}
fn contextual(&mut self, node: &SyntaxNode) -> Result<Value> {
if self.feat.remove_html {
return self.to_raw_block(node, false);
}
// Trim the last `#` in the prepend code. (#context)
self.prepend_code = self.prepend_code.trim_end_matches('#').into();
self.render(
&SyntaxNode::leaf(node.kind(), self.prepend_code.clone()),
node,
false,
)
}
fn include(&self, node: &SyntaxNode) -> Result<Value> {
let include: ast::ModuleInclude = node.cast().unwrap();
let path = include.source();
let src =
tinymist_analysis::syntax::find_source_by_expr(self.world.as_ref(), self.current, path)
.ok_or_else(|| format!("failed to find source on path {path:?}"))?;
self.clone().sub_file(src).map(Value::Content)
}
fn sub_file(mut self, src: Source) -> Result<EcoString> {
self.current = src.id();
self.convert(src.root())
}
}
struct WrapCode<'a>(&'a str, bool);
impl fmt::Display for WrapCode<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let is_markup = self.1;
if is_markup {
f.write_str("#[")?;
} else {
f.write_str("#{")?;
}
f.write_str(self.0)?;
if is_markup {
f.write_str("]")
} else {
f.write_str("}")
}
}
}