typst 0.0.4 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|