typst 0.0.4 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +38 -4
- data/README.typ +38 -4
- data/ext/typst/Cargo.toml +30 -15
- data/ext/typst/src/compiler.rs +100 -39
- data/ext/typst/src/download.rs +11 -71
- data/ext/typst/src/lib.rs +218 -21
- data/ext/typst/src/query.rs +131 -0
- data/ext/typst/src/world.rs +130 -136
- data/lib/typst.rb +63 -4
- metadata +4 -8
- data/ext/typst/src/lib.wip.rs +0 -203
data/ext/typst/src/lib.rs
CHANGED
@@ -1,22 +1,94 @@
|
|
1
1
|
use std::path::PathBuf;
|
2
2
|
|
3
|
-
use magnus::{define_module, function, exception, Error
|
3
|
+
use magnus::{define_module, function, exception, Error }; //, IntoValue};
|
4
4
|
use magnus::{prelude::*};
|
5
5
|
|
6
|
+
use std::collections::HashMap;
|
7
|
+
use query::{query as typst_query, QueryCommand, SerializationFormat};
|
8
|
+
use typst::foundations::{Dict, Value};
|
9
|
+
use typst_library::Feature;
|
6
10
|
use world::SystemWorld;
|
7
11
|
|
8
12
|
mod compiler;
|
9
13
|
mod download;
|
10
|
-
mod
|
11
|
-
mod package;
|
14
|
+
mod query;
|
12
15
|
mod world;
|
13
16
|
|
17
|
+
fn to_html(
|
18
|
+
input: PathBuf,
|
19
|
+
root: Option<PathBuf>,
|
20
|
+
font_paths: Vec<PathBuf>,
|
21
|
+
resource_path: PathBuf,
|
22
|
+
ignore_system_fonts: bool,
|
23
|
+
sys_inputs: HashMap<String, String>,
|
24
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
25
|
+
let input = input.canonicalize()
|
26
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
27
|
+
|
28
|
+
let root = if let Some(root) = root {
|
29
|
+
root.canonicalize()
|
30
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
31
|
+
} else if let Some(dir) = input.parent() {
|
32
|
+
dir.into()
|
33
|
+
} else {
|
34
|
+
PathBuf::new()
|
35
|
+
};
|
36
|
+
|
37
|
+
let resource_path = resource_path.canonicalize()
|
38
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
39
|
+
|
40
|
+
let mut default_fonts = Vec::new();
|
41
|
+
for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
|
42
|
+
let path = entry
|
43
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
44
|
+
.into_path();
|
45
|
+
let Some(extension) = path.extension() else {
|
46
|
+
continue;
|
47
|
+
};
|
48
|
+
if extension == "ttf" || extension == "otf" {
|
49
|
+
default_fonts.push(path);
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
let mut features = Vec::new();
|
54
|
+
features.push(Feature::Html);
|
55
|
+
|
56
|
+
let feat = features.iter()
|
57
|
+
.map(|&feature|
|
58
|
+
match feature {
|
59
|
+
Feature::Html => typst::Feature::Html,
|
60
|
+
_ => typst::Feature::Html // TODO: fix this hack
|
61
|
+
}
|
62
|
+
)
|
63
|
+
.collect();
|
64
|
+
|
65
|
+
let mut world = SystemWorld::builder(root, input)
|
66
|
+
.inputs(Dict::from_iter(
|
67
|
+
sys_inputs
|
68
|
+
.into_iter()
|
69
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
70
|
+
))
|
71
|
+
.features(feat)
|
72
|
+
.font_paths(font_paths)
|
73
|
+
.ignore_system_fonts(ignore_system_fonts)
|
74
|
+
.build()
|
75
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
76
|
+
|
77
|
+
let bytes = world
|
78
|
+
.compile(Some("html"), None, &Vec::new())
|
79
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
80
|
+
|
81
|
+
Ok(bytes)
|
82
|
+
}
|
83
|
+
|
14
84
|
fn to_svg(
|
15
85
|
input: PathBuf,
|
16
86
|
root: Option<PathBuf>,
|
17
87
|
font_paths: Vec<PathBuf>,
|
18
88
|
resource_path: PathBuf,
|
19
|
-
|
89
|
+
ignore_system_fonts: bool,
|
90
|
+
sys_inputs: HashMap<String, String>,
|
91
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
20
92
|
let input = input.canonicalize()
|
21
93
|
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
22
94
|
|
@@ -46,24 +118,85 @@ fn to_svg(
|
|
46
118
|
}
|
47
119
|
|
48
120
|
let mut world = SystemWorld::builder(root, input)
|
121
|
+
.inputs(Dict::from_iter(
|
122
|
+
sys_inputs
|
123
|
+
.into_iter()
|
124
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
125
|
+
))
|
49
126
|
.font_paths(font_paths)
|
50
|
-
.
|
127
|
+
.ignore_system_fonts(ignore_system_fonts)
|
51
128
|
.build()
|
52
129
|
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
53
130
|
|
54
131
|
let svg_bytes = world
|
55
|
-
.
|
132
|
+
.compile(Some("svg"), None, &Vec::new())
|
56
133
|
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
57
134
|
|
58
135
|
Ok(svg_bytes)
|
59
136
|
}
|
60
137
|
|
138
|
+
fn to_png(
|
139
|
+
input: PathBuf,
|
140
|
+
root: Option<PathBuf>,
|
141
|
+
font_paths: Vec<PathBuf>,
|
142
|
+
resource_path: PathBuf,
|
143
|
+
ignore_system_fonts: bool,
|
144
|
+
sys_inputs: HashMap<String, String>,
|
145
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
146
|
+
let input = input.canonicalize()
|
147
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
148
|
+
|
149
|
+
let root = if let Some(root) = root {
|
150
|
+
root.canonicalize()
|
151
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
152
|
+
} else if let Some(dir) = input.parent() {
|
153
|
+
dir.into()
|
154
|
+
} else {
|
155
|
+
PathBuf::new()
|
156
|
+
};
|
157
|
+
|
158
|
+
let resource_path = resource_path.canonicalize()
|
159
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
160
|
+
|
161
|
+
let mut default_fonts = Vec::new();
|
162
|
+
for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
|
163
|
+
let path = entry
|
164
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
165
|
+
.into_path();
|
166
|
+
let Some(extension) = path.extension() else {
|
167
|
+
continue;
|
168
|
+
};
|
169
|
+
if extension == "ttf" || extension == "otf" {
|
170
|
+
default_fonts.push(path);
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
let mut world = SystemWorld::builder(root, input)
|
175
|
+
.inputs(Dict::from_iter(
|
176
|
+
sys_inputs
|
177
|
+
.into_iter()
|
178
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
179
|
+
))
|
180
|
+
.font_paths(font_paths)
|
181
|
+
.ignore_system_fonts(ignore_system_fonts)
|
182
|
+
.build()
|
183
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
184
|
+
|
185
|
+
let bytes = world
|
186
|
+
.compile(Some("png"), None, &Vec::new())
|
187
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
188
|
+
|
189
|
+
Ok(bytes)
|
190
|
+
}
|
191
|
+
|
61
192
|
fn to_pdf(
|
62
193
|
input: PathBuf,
|
63
194
|
root: Option<PathBuf>,
|
64
195
|
font_paths: Vec<PathBuf>,
|
65
196
|
resource_path: PathBuf,
|
66
|
-
|
197
|
+
ignore_system_fonts: bool,
|
198
|
+
sys_inputs: HashMap<String, String>,
|
199
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
67
200
|
let input = input.canonicalize()
|
68
201
|
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
69
202
|
|
@@ -93,41 +226,105 @@ fn to_pdf(
|
|
93
226
|
}
|
94
227
|
|
95
228
|
let mut world = SystemWorld::builder(root, input)
|
229
|
+
.inputs(Dict::from_iter(
|
230
|
+
sys_inputs
|
231
|
+
.into_iter()
|
232
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
233
|
+
))
|
96
234
|
.font_paths(font_paths)
|
97
|
-
.
|
235
|
+
.ignore_system_fonts(ignore_system_fonts)
|
98
236
|
.build()
|
99
237
|
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
100
238
|
|
101
239
|
let pdf_bytes = world
|
102
|
-
.
|
240
|
+
.compile(Some("pdf"), None, &Vec::new())
|
103
241
|
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
104
242
|
|
105
243
|
Ok(pdf_bytes)
|
106
244
|
}
|
107
245
|
|
108
|
-
fn
|
246
|
+
fn query(
|
247
|
+
selector: String,
|
248
|
+
field: Option<String>,
|
249
|
+
one: bool,
|
250
|
+
format: Option<String>,
|
109
251
|
input: PathBuf,
|
110
|
-
output: PathBuf,
|
111
252
|
root: Option<PathBuf>,
|
112
253
|
font_paths: Vec<PathBuf>,
|
113
254
|
resource_path: PathBuf,
|
114
|
-
|
115
|
-
|
255
|
+
ignore_system_fonts: bool,
|
256
|
+
sys_inputs: HashMap<String, String>,
|
257
|
+
) -> Result<String, Error> {
|
258
|
+
let format = match format.unwrap().to_ascii_lowercase().as_str() {
|
259
|
+
"json" => SerializationFormat::Json,
|
260
|
+
"yaml" => SerializationFormat::Yaml,
|
261
|
+
_ => return Err(magnus::Error::new(exception::arg_error(), "unsupported serialization format"))?,
|
262
|
+
};
|
116
263
|
|
117
|
-
|
118
|
-
.map_err(|
|
264
|
+
let input = input.canonicalize()
|
265
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
119
266
|
|
120
|
-
let
|
121
|
-
|
122
|
-
|
267
|
+
let root = if let Some(root) = root {
|
268
|
+
root.canonicalize()
|
269
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
270
|
+
} else if let Some(dir) = input.parent() {
|
271
|
+
dir.into()
|
272
|
+
} else {
|
273
|
+
PathBuf::new()
|
274
|
+
};
|
275
|
+
|
276
|
+
let resource_path = resource_path.canonicalize()
|
277
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
278
|
+
|
279
|
+
let mut default_fonts = Vec::new();
|
280
|
+
for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
|
281
|
+
let path = entry
|
282
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
283
|
+
.into_path();
|
284
|
+
let Some(extension) = path.extension() else {
|
285
|
+
continue;
|
286
|
+
};
|
287
|
+
if extension == "ttf" || extension == "otf" {
|
288
|
+
default_fonts.push(path);
|
289
|
+
}
|
290
|
+
}
|
291
|
+
|
292
|
+
let mut world = SystemWorld::builder(root, input)
|
293
|
+
.inputs(Dict::from_iter(
|
294
|
+
sys_inputs
|
295
|
+
.into_iter()
|
296
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
297
|
+
))
|
298
|
+
.font_paths(font_paths)
|
299
|
+
.ignore_system_fonts(ignore_system_fonts)
|
300
|
+
.build()
|
301
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
302
|
+
|
303
|
+
let result = typst_query(
|
304
|
+
&mut world,
|
305
|
+
&QueryCommand {
|
306
|
+
selector: selector.into(),
|
307
|
+
field: field.map(Into::into),
|
308
|
+
one,
|
309
|
+
format,
|
310
|
+
},
|
311
|
+
);
|
312
|
+
|
313
|
+
match result {
|
314
|
+
Ok(data) => Ok(data),
|
315
|
+
Err(msg) => Err(magnus::Error::new(exception::arg_error(), msg.to_string())),
|
316
|
+
}
|
317
|
+
}
|
123
318
|
|
124
319
|
#[magnus::init]
|
125
320
|
fn init() -> Result<(), Error> {
|
126
321
|
env_logger::init();
|
127
322
|
|
128
323
|
let module = define_module("Typst")?;
|
129
|
-
module.define_singleton_method("_to_pdf", function!(to_pdf,
|
130
|
-
module.define_singleton_method("
|
131
|
-
module.define_singleton_method("
|
324
|
+
module.define_singleton_method("_to_pdf", function!(to_pdf, 6))?;
|
325
|
+
module.define_singleton_method("_to_svg", function!(to_svg, 6))?;
|
326
|
+
module.define_singleton_method("_to_png", function!(to_png, 6))?;
|
327
|
+
module.define_singleton_method("_to_html", function!(to_html, 6))?;
|
328
|
+
module.define_singleton_method("_query", function!(query, 10))?;
|
132
329
|
Ok(())
|
133
330
|
}
|
@@ -0,0 +1,131 @@
|
|
1
|
+
use comemo::Track;
|
2
|
+
use ecow::{eco_format, EcoString};
|
3
|
+
use serde::Serialize;
|
4
|
+
use typst::diag::{bail, StrResult, Warned};
|
5
|
+
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
|
6
|
+
use typst::layout::PagedDocument;
|
7
|
+
use typst::syntax::Span;
|
8
|
+
use typst::World;
|
9
|
+
use typst_eval::{eval_string, EvalMode};
|
10
|
+
|
11
|
+
use crate::world::SystemWorld;
|
12
|
+
|
13
|
+
/// Processes an input file to extract provided metadata
|
14
|
+
#[derive(Debug, Clone)]
|
15
|
+
pub struct QueryCommand {
|
16
|
+
/// Defines which elements to retrieve
|
17
|
+
pub selector: String,
|
18
|
+
|
19
|
+
/// Extracts just one field from all retrieved elements
|
20
|
+
pub field: Option<String>,
|
21
|
+
|
22
|
+
/// Expects and retrieves exactly one element
|
23
|
+
pub one: bool,
|
24
|
+
|
25
|
+
/// The format to serialize in
|
26
|
+
pub format: SerializationFormat,
|
27
|
+
}
|
28
|
+
|
29
|
+
// Output file format for query command
|
30
|
+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
31
|
+
pub enum SerializationFormat {
|
32
|
+
Json,
|
33
|
+
Yaml,
|
34
|
+
}
|
35
|
+
|
36
|
+
/// Execute a query command.
|
37
|
+
pub fn query(world: &mut SystemWorld, command: &QueryCommand) -> StrResult<String> {
|
38
|
+
// Reset everything and ensure that the main file is present.
|
39
|
+
world.reset();
|
40
|
+
world.source(world.main()).map_err(|err| err.to_string())?;
|
41
|
+
|
42
|
+
let Warned { output, warnings } = typst::compile(world);
|
43
|
+
|
44
|
+
match output {
|
45
|
+
// Retrieve and print query results.
|
46
|
+
Ok(document) => {
|
47
|
+
let data = retrieve(world, command, &document)?;
|
48
|
+
let serialized = format(data, command)?;
|
49
|
+
Ok(serialized)
|
50
|
+
}
|
51
|
+
// Print errors and warnings.
|
52
|
+
Err(errors) => {
|
53
|
+
let mut message = EcoString::from("failed to compile document");
|
54
|
+
for (i, error) in errors.into_iter().enumerate() {
|
55
|
+
message.push_str(if i == 0 { ": " } else { ", " });
|
56
|
+
message.push_str(&error.message);
|
57
|
+
}
|
58
|
+
for warning in warnings {
|
59
|
+
message.push_str(": ");
|
60
|
+
message.push_str(&warning.message);
|
61
|
+
}
|
62
|
+
Err(message)
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
/// Retrieve the matches for the selector.
|
68
|
+
fn retrieve(
|
69
|
+
world: &dyn World,
|
70
|
+
command: &QueryCommand,
|
71
|
+
document: &PagedDocument,
|
72
|
+
) -> StrResult<Vec<Content>> {
|
73
|
+
let selector = eval_string(
|
74
|
+
&typst::ROUTINES,
|
75
|
+
world.track(),
|
76
|
+
&command.selector,
|
77
|
+
Span::detached(),
|
78
|
+
EvalMode::Code,
|
79
|
+
Scope::default(),
|
80
|
+
)
|
81
|
+
.map_err(|errors| {
|
82
|
+
let mut message = EcoString::from("failed to evaluate selector");
|
83
|
+
for (i, error) in errors.into_iter().enumerate() {
|
84
|
+
message.push_str(if i == 0 { ": " } else { ", " });
|
85
|
+
message.push_str(&error.message);
|
86
|
+
}
|
87
|
+
message
|
88
|
+
})?
|
89
|
+
.cast::<LocatableSelector>()
|
90
|
+
.map_err(|e| e.message().clone())?;
|
91
|
+
|
92
|
+
Ok(document
|
93
|
+
.introspector
|
94
|
+
.query(&selector.0)
|
95
|
+
.into_iter()
|
96
|
+
.collect::<Vec<_>>())
|
97
|
+
}
|
98
|
+
|
99
|
+
/// Format the query result in the output format.
|
100
|
+
fn format(elements: Vec<Content>, command: &QueryCommand) -> StrResult<String> {
|
101
|
+
if command.one && elements.len() != 1 {
|
102
|
+
bail!("expected exactly one element, found {}", elements.len());
|
103
|
+
}
|
104
|
+
|
105
|
+
let mapped: Vec<_> = elements
|
106
|
+
.into_iter()
|
107
|
+
.filter_map(|c| match &command.field {
|
108
|
+
Some(field) => c.get_by_name(field).ok(),
|
109
|
+
_ => Some(c.into_value()),
|
110
|
+
})
|
111
|
+
.collect();
|
112
|
+
|
113
|
+
if command.one {
|
114
|
+
let Some(value) = mapped.first() else {
|
115
|
+
bail!("no such field found for element");
|
116
|
+
};
|
117
|
+
serialize(value, command.format)
|
118
|
+
} else {
|
119
|
+
serialize(&mapped, command.format)
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
/// Serialize data to the output format.
|
124
|
+
fn serialize(data: &impl Serialize, format: SerializationFormat) -> StrResult<String> {
|
125
|
+
match format {
|
126
|
+
SerializationFormat::Json => {
|
127
|
+
serde_json::to_string_pretty(data).map_err(|e| eco_format!("{e}"))
|
128
|
+
}
|
129
|
+
SerializationFormat::Yaml => serde_yaml::to_string(&data).map_err(|e| eco_format!("{e}")),
|
130
|
+
}
|
131
|
+
}
|