typst-rb 0.15.0.0.3
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 +7 -0
- data/Cargo.lock +4136 -0
- data/Cargo.toml +3 -0
- data/LICENSE +176 -0
- data/README.md +200 -0
- data/README.typ +208 -0
- data/Rakefile +43 -0
- data/ext/typst/Cargo.toml +55 -0
- data/ext/typst/extconf.rb +4 -0
- data/ext/typst/src/compiler.rs +257 -0
- data/ext/typst/src/download.rs +7 -0
- data/ext/typst/src/lib.rs +296 -0
- data/ext/typst/src/package.rs +64 -0
- data/ext/typst/src/query.rs +138 -0
- data/ext/typst/src/world.rs +400 -0
- data/lib/base.rb +142 -0
- data/lib/document.rb +37 -0
- data/lib/formats/html_experimental.rb +13 -0
- data/lib/formats/pdf.rb +13 -0
- data/lib/formats/png.rb +13 -0
- data/lib/formats/svg.rb +13 -0
- data/lib/query.rb +22 -0
- data/lib/typst-rb.rb +6 -0
- data/lib/typst.rb +101 -0
- metadata +126 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
use comemo::Track;
|
|
2
|
+
use ecow::{eco_format, EcoString};
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
use typst::diag::{bail, StrResult, Warned};
|
|
5
|
+
use typst::engine::Sink;
|
|
6
|
+
use typst::foundations::{Content, Context, IntoValue, LocatableSelector, Scope};
|
|
7
|
+
use typst::introspection::{EmptyIntrospector, Introspector};
|
|
8
|
+
use typst::routines::SpanMode;
|
|
9
|
+
use typst_layout::PagedDocument;
|
|
10
|
+
use typst::syntax::Span;
|
|
11
|
+
use typst::syntax::SyntaxMode;
|
|
12
|
+
use typst::World;
|
|
13
|
+
use typst_eval::eval_string;
|
|
14
|
+
|
|
15
|
+
use crate::world::SystemWorld;
|
|
16
|
+
|
|
17
|
+
/// Processes an input file to extract provided metadata
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct QueryCommand {
|
|
20
|
+
/// Defines which elements to retrieve
|
|
21
|
+
pub selector: String,
|
|
22
|
+
|
|
23
|
+
/// Extracts just one field from all retrieved elements
|
|
24
|
+
pub field: Option<String>,
|
|
25
|
+
|
|
26
|
+
/// Expects and retrieves exactly one element
|
|
27
|
+
pub one: bool,
|
|
28
|
+
|
|
29
|
+
/// The format to serialize in
|
|
30
|
+
pub format: SerializationFormat,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Output file format for query command
|
|
34
|
+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
35
|
+
pub enum SerializationFormat {
|
|
36
|
+
Json,
|
|
37
|
+
Yaml,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Execute a query command.
|
|
41
|
+
pub fn query(world: &mut SystemWorld, command: &QueryCommand) -> StrResult<String> {
|
|
42
|
+
// Reset everything and ensure that the main file is present.
|
|
43
|
+
world.reset();
|
|
44
|
+
world.source(world.main()).map_err(|err| err.to_string())?;
|
|
45
|
+
|
|
46
|
+
let Warned { output, warnings } = typst::compile(world);
|
|
47
|
+
|
|
48
|
+
match output {
|
|
49
|
+
// Retrieve and print query results.
|
|
50
|
+
Ok(document) => {
|
|
51
|
+
let data = retrieve(world, command, &document)?;
|
|
52
|
+
let serialized = format(data, command)?;
|
|
53
|
+
Ok(serialized)
|
|
54
|
+
}
|
|
55
|
+
// Print errors and warnings.
|
|
56
|
+
Err(errors) => {
|
|
57
|
+
let mut message = EcoString::from("failed to compile document");
|
|
58
|
+
for (i, error) in errors.into_iter().enumerate() {
|
|
59
|
+
message.push_str(if i == 0 { ": " } else { ", " });
|
|
60
|
+
message.push_str(&error.message);
|
|
61
|
+
}
|
|
62
|
+
for warning in warnings {
|
|
63
|
+
message.push_str(": ");
|
|
64
|
+
message.push_str(&warning.message);
|
|
65
|
+
}
|
|
66
|
+
Err(message)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Retrieve the matches for the selector.
|
|
72
|
+
fn retrieve(
|
|
73
|
+
world: &dyn World,
|
|
74
|
+
command: &QueryCommand,
|
|
75
|
+
document: &PagedDocument,
|
|
76
|
+
) -> StrResult<Vec<Content>> {
|
|
77
|
+
let selector = eval_string(
|
|
78
|
+
world.track(),
|
|
79
|
+
world.library(),
|
|
80
|
+
Sink::new().track_mut(),
|
|
81
|
+
EmptyIntrospector.track(),
|
|
82
|
+
Context::none().track(),
|
|
83
|
+
&command.selector,
|
|
84
|
+
SpanMode::Uniform(Span::detached()),
|
|
85
|
+
SyntaxMode::Code,
|
|
86
|
+
Scope::default()
|
|
87
|
+
)
|
|
88
|
+
.map_err(|errors| {
|
|
89
|
+
let mut message = EcoString::from("failed to evaluate selector");
|
|
90
|
+
for (i, error) in errors.into_iter().enumerate() {
|
|
91
|
+
message.push_str(if i == 0 { ": " } else { ", " });
|
|
92
|
+
message.push_str(&error.message);
|
|
93
|
+
}
|
|
94
|
+
message
|
|
95
|
+
})?
|
|
96
|
+
.cast::<LocatableSelector>()
|
|
97
|
+
.map_err(|e| e.message().clone())?;
|
|
98
|
+
|
|
99
|
+
Ok(document
|
|
100
|
+
.introspector()
|
|
101
|
+
.query(&selector.0)
|
|
102
|
+
.into_iter()
|
|
103
|
+
.collect::<Vec<_>>())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Format the query result in the output format.
|
|
107
|
+
fn format(elements: Vec<Content>, command: &QueryCommand) -> StrResult<String> {
|
|
108
|
+
if command.one && elements.len() != 1 {
|
|
109
|
+
bail!("expected exactly one element, found {}", elements.len());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let mapped: Vec<_> = elements
|
|
113
|
+
.into_iter()
|
|
114
|
+
.filter_map(|c| match &command.field {
|
|
115
|
+
Some(field) => c.get_by_name(field).ok(),
|
|
116
|
+
_ => Some(c.into_value()),
|
|
117
|
+
})
|
|
118
|
+
.collect();
|
|
119
|
+
|
|
120
|
+
if command.one {
|
|
121
|
+
let Some(value) = mapped.first() else {
|
|
122
|
+
bail!("no such field found for element");
|
|
123
|
+
};
|
|
124
|
+
serialize(value, command.format)
|
|
125
|
+
} else {
|
|
126
|
+
serialize(&mapped, command.format)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Serialize data to the output format.
|
|
131
|
+
fn serialize(data: &impl Serialize, format: SerializationFormat) -> StrResult<String> {
|
|
132
|
+
match format {
|
|
133
|
+
SerializationFormat::Json => {
|
|
134
|
+
serde_json::to_string_pretty(data).map_err(|e| eco_format!("{e}"))
|
|
135
|
+
}
|
|
136
|
+
SerializationFormat::Yaml => serde_yaml::to_string(&data).map_err(|e| eco_format!("{e}")),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
|
4
|
+
use std::collections::HashMap;
|
|
5
|
+
|
|
6
|
+
use chrono::{DateTime, Datelike, FixedOffset, Local};
|
|
7
|
+
use typst::diag::{FileError, FileResult, StrResult};
|
|
8
|
+
use typst::foundations::{Bytes, Datetime, Dict, Duration};
|
|
9
|
+
use typst::syntax::{FileId, Lines, Source, VirtualPath, VirtualRoot, RootedPath};
|
|
10
|
+
use typst::text::{Font, FontBook};
|
|
11
|
+
use typst::utils::LazyHash;
|
|
12
|
+
use typst::{Features, Library, LibraryExt, World};
|
|
13
|
+
use typst_kit::fonts::{self, FontStore};
|
|
14
|
+
use typst_kit::packages::{FsPackages, SystemPackages, UniversePackages};
|
|
15
|
+
|
|
16
|
+
/// A world that provides access to the operating system.
|
|
17
|
+
pub struct SystemWorld {
|
|
18
|
+
/// The working directory.
|
|
19
|
+
workdir: Option<PathBuf>,
|
|
20
|
+
/// The root relative to which absolute paths are resolved.
|
|
21
|
+
root: PathBuf,
|
|
22
|
+
/// The input path.
|
|
23
|
+
main: FileId,
|
|
24
|
+
/// Typst's standard library.
|
|
25
|
+
library: LazyHash<Library>,
|
|
26
|
+
/// Locations of and storage for lazily loaded fonts.
|
|
27
|
+
fonts: Arc<FontStore>,
|
|
28
|
+
/// Maps file ids to source files and buffers.
|
|
29
|
+
slots: Mutex<HashMap<FileId, FileSlot>>,
|
|
30
|
+
/// Holds information about where packages are stored.
|
|
31
|
+
package_storage: SystemPackages,
|
|
32
|
+
/// The current datetime if requested. This is stored here to ensure it is
|
|
33
|
+
/// always the same within one compilation. Reset between compilations.
|
|
34
|
+
now: OnceLock<DateTime<Local>>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl World for SystemWorld {
|
|
38
|
+
fn library(&self) -> &LazyHash<Library> {
|
|
39
|
+
&self.library
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn book(&self) -> &LazyHash<FontBook> {
|
|
43
|
+
&self.fonts.book()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn main(&self) -> FileId {
|
|
47
|
+
self.main
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn source(&self, id: FileId) -> FileResult<Source> {
|
|
51
|
+
self.slot(id, |slot| slot.source(&self.root, &self.package_storage))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
|
55
|
+
self.slot(id, |slot| slot.file(&self.root, &self.package_storage))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn font(&self, index: usize) -> Option<Font> {
|
|
59
|
+
self.fonts.font(index)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
|
|
63
|
+
let now = self.now.get_or_init(chrono::Local::now);
|
|
64
|
+
|
|
65
|
+
let now = match offset {
|
|
66
|
+
None => now.fixed_offset(),
|
|
67
|
+
Some(offset) => {
|
|
68
|
+
let seconds = offset.seconds().trunc();
|
|
69
|
+
if !seconds.is_finite()
|
|
70
|
+
|| seconds < f64::from(i32::MIN)
|
|
71
|
+
|| seconds > f64::from(i32::MAX)
|
|
72
|
+
{
|
|
73
|
+
return None;
|
|
74
|
+
}
|
|
75
|
+
now.with_timezone(&FixedOffset::east_opt(seconds as i32)?)
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
Datetime::from_ymd(
|
|
80
|
+
now.year(),
|
|
81
|
+
now.month().try_into().ok()?,
|
|
82
|
+
now.day().try_into().ok()?,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
impl SystemWorld {
|
|
88
|
+
pub fn builder(root: PathBuf, main: PathBuf) -> SystemWorldBuilder {
|
|
89
|
+
SystemWorldBuilder::new(root, main)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Access the canonical slot for the given file id.
|
|
93
|
+
fn slot<F, T>(&self, id: FileId, f: F) -> T
|
|
94
|
+
where
|
|
95
|
+
F: FnOnce(&mut FileSlot) -> T,
|
|
96
|
+
{
|
|
97
|
+
let mut map = self.slots.lock().unwrap();
|
|
98
|
+
f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// The id of the main source file.
|
|
102
|
+
pub fn main(&self) -> FileId {
|
|
103
|
+
self.main
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// The root relative to which absolute paths are resolved.
|
|
107
|
+
pub fn root(&self) -> &Path {
|
|
108
|
+
&self.root
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// The current working directory.
|
|
112
|
+
pub fn workdir(&self) -> &Path {
|
|
113
|
+
self.workdir.as_deref().unwrap_or(Path::new("."))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Reset the compilation state in preparation of a new compilation.
|
|
117
|
+
pub fn reset(&mut self) {
|
|
118
|
+
for slot in self.slots.lock().unwrap().values_mut() {
|
|
119
|
+
slot.reset();
|
|
120
|
+
}
|
|
121
|
+
self.now.take();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Lookup a source file by id.
|
|
125
|
+
#[track_caller]
|
|
126
|
+
pub fn lookup(&self, id: FileId) -> Lines<String> {
|
|
127
|
+
self.slot(id, |slot| {
|
|
128
|
+
if let Some(source) = slot.source.get() {
|
|
129
|
+
let source = source.as_ref().expect("file is not valid");
|
|
130
|
+
source.lines().clone()
|
|
131
|
+
} else if let Some(bytes) = slot.file.get() {
|
|
132
|
+
let bytes = bytes.as_ref().expect("file is not valid");
|
|
133
|
+
Lines::new(
|
|
134
|
+
decode_utf8(bytes.as_slice())
|
|
135
|
+
.expect("file is not valid utf-8")
|
|
136
|
+
.to_string(),
|
|
137
|
+
)
|
|
138
|
+
} else {
|
|
139
|
+
panic!("file id does not point to any source file");
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub struct SystemWorldBuilder {
|
|
146
|
+
root: PathBuf,
|
|
147
|
+
main: PathBuf,
|
|
148
|
+
font_paths: Vec<PathBuf>,
|
|
149
|
+
ignore_system_fonts: bool,
|
|
150
|
+
ignore_embedded_fonts: bool,
|
|
151
|
+
inputs: Dict,
|
|
152
|
+
features: Features,
|
|
153
|
+
package_path: Option<PathBuf>,
|
|
154
|
+
package_cache_path: Option<PathBuf>,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
impl SystemWorldBuilder {
|
|
158
|
+
pub fn new(root: PathBuf, main: PathBuf) -> Self {
|
|
159
|
+
Self {
|
|
160
|
+
root,
|
|
161
|
+
main,
|
|
162
|
+
font_paths: Vec::new(),
|
|
163
|
+
ignore_system_fonts: false,
|
|
164
|
+
ignore_embedded_fonts: false,
|
|
165
|
+
inputs: Dict::default(),
|
|
166
|
+
features: Features::default(),
|
|
167
|
+
package_path: None,
|
|
168
|
+
package_cache_path: None,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pub fn font_paths(mut self, font_paths: Vec<PathBuf>) -> Self {
|
|
173
|
+
self.font_paths = font_paths;
|
|
174
|
+
self
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pub fn ignore_system_fonts(mut self, ignore: bool) -> Self {
|
|
178
|
+
self.ignore_system_fonts = ignore;
|
|
179
|
+
self
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
pub fn ignore_embedded_fonts(mut self, ignore: bool) -> Self {
|
|
183
|
+
self.ignore_embedded_fonts = ignore;
|
|
184
|
+
self
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
pub fn inputs(mut self, inputs: Dict) -> Self {
|
|
188
|
+
self.inputs = inputs;
|
|
189
|
+
self
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
pub fn features(mut self, features: Features) -> Self {
|
|
193
|
+
self.features = features;
|
|
194
|
+
self
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
pub fn build(self) -> StrResult<SystemWorld> {
|
|
198
|
+
let fonts = Arc::new(build_font_store(self.ignore_system_fonts, self.ignore_embedded_fonts, self.font_paths));
|
|
199
|
+
|
|
200
|
+
let package_storage = system_packages(self.package_path, self.package_cache_path);
|
|
201
|
+
|
|
202
|
+
// Resolve the virtual path of the main file within the project root.
|
|
203
|
+
let main_path = VirtualPath::virtualize(&self.root, &self.main)
|
|
204
|
+
.map_err(|_| "input file must be contained in project root")?;
|
|
205
|
+
|
|
206
|
+
let world = SystemWorld {
|
|
207
|
+
workdir: std::env::current_dir().ok(),
|
|
208
|
+
root: self.root,
|
|
209
|
+
main: RootedPath::new(VirtualRoot::Project, main_path).intern(),
|
|
210
|
+
library: LazyHash::new(Library::builder().with_inputs(self.inputs).with_features(self.features).build()),
|
|
211
|
+
fonts,
|
|
212
|
+
slots: Mutex::default(),
|
|
213
|
+
package_storage,
|
|
214
|
+
now: OnceLock::new(),
|
|
215
|
+
};
|
|
216
|
+
Ok(world)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn build_font_store(ignore_system_fonts: bool, ignore_embedded_fonts: bool, font_paths: Vec<PathBuf>) -> FontStore {
|
|
221
|
+
let mut fonts = FontStore::new();
|
|
222
|
+
if !ignore_system_fonts {
|
|
223
|
+
fonts.extend(fonts::system());
|
|
224
|
+
}
|
|
225
|
+
if !ignore_embedded_fonts {
|
|
226
|
+
fonts.extend(fonts::embedded());
|
|
227
|
+
}
|
|
228
|
+
for path in font_paths {
|
|
229
|
+
fonts.extend(fonts::scan(&path));
|
|
230
|
+
}
|
|
231
|
+
fonts
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn system_packages(
|
|
235
|
+
package_path: Option<PathBuf>,
|
|
236
|
+
package_cache_path: Option<PathBuf>,
|
|
237
|
+
) -> SystemPackages {
|
|
238
|
+
SystemPackages::from_parts(
|
|
239
|
+
package_path
|
|
240
|
+
.map(FsPackages::new)
|
|
241
|
+
.or_else(FsPackages::system_data),
|
|
242
|
+
package_cache_path
|
|
243
|
+
.map(FsPackages::new)
|
|
244
|
+
.or_else(FsPackages::system_cache),
|
|
245
|
+
UniversePackages::new(crate::download::downloader()),
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Holds canonical data for all paths pointing to the same entity.
|
|
250
|
+
///
|
|
251
|
+
/// Both fields can be populated if the file is both imported and read().
|
|
252
|
+
struct FileSlot {
|
|
253
|
+
/// The slot's canonical file id.
|
|
254
|
+
id: FileId,
|
|
255
|
+
/// The lazily loaded and incrementally updated source file.
|
|
256
|
+
source: SlotCell<Source>,
|
|
257
|
+
/// The lazily loaded raw byte buffer.
|
|
258
|
+
file: SlotCell<Bytes>,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
impl FileSlot {
|
|
262
|
+
/// Create a new path slot.
|
|
263
|
+
fn new(id: FileId) -> Self {
|
|
264
|
+
Self {
|
|
265
|
+
id,
|
|
266
|
+
file: SlotCell::new(),
|
|
267
|
+
source: SlotCell::new(),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Marks the file as not yet accessed in preparation of the next
|
|
272
|
+
/// compilation.
|
|
273
|
+
fn reset(&mut self) {
|
|
274
|
+
self.source.reset();
|
|
275
|
+
self.file.reset();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn source(&mut self, root: &Path, package_storage: &SystemPackages) -> FileResult<Source> {
|
|
279
|
+
let id = self.id;
|
|
280
|
+
self.source.get_or_init(
|
|
281
|
+
|| system_path(root, id, package_storage),
|
|
282
|
+
|data, prev| {
|
|
283
|
+
let text = decode_utf8(&data)?;
|
|
284
|
+
if let Some(mut prev) = prev {
|
|
285
|
+
prev.replace(text);
|
|
286
|
+
Ok(prev)
|
|
287
|
+
} else {
|
|
288
|
+
Ok(Source::new(self.id, text.into()))
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fn file(&mut self, root: &Path, package_storage: &SystemPackages) -> FileResult<Bytes> {
|
|
295
|
+
let id = self.id;
|
|
296
|
+
self.file.get_or_init(
|
|
297
|
+
|| system_path(root, id, package_storage),
|
|
298
|
+
|data, _| Ok(Bytes::new(data)),
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// The path of the slot on the system.
|
|
304
|
+
fn system_path(root: &Path, id: FileId, package_storage: &SystemPackages) -> FileResult<PathBuf> {
|
|
305
|
+
// Determine the root path relative to which the file path
|
|
306
|
+
// will be resolved.
|
|
307
|
+
let package_root;
|
|
308
|
+
let root = match id.root() {
|
|
309
|
+
VirtualRoot::Project => root,
|
|
310
|
+
VirtualRoot::Package(spec) => {
|
|
311
|
+
package_root = package_storage.obtain(spec)?;
|
|
312
|
+
package_root.path()
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Join the path to the root. If it tries to escape, deny
|
|
317
|
+
// access. Note: It can still escape via symlinks.
|
|
318
|
+
id.vpath().realize(root).map_err(Into::into)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Lazily processes data for a file.
|
|
322
|
+
struct SlotCell<T> {
|
|
323
|
+
/// The processed data.
|
|
324
|
+
data: Option<FileResult<T>>,
|
|
325
|
+
/// A hash of the raw file contents / access error.
|
|
326
|
+
fingerprint: u128,
|
|
327
|
+
/// Whether the slot has been accessed in the current compilation.
|
|
328
|
+
accessed: bool,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
impl<T: Clone> SlotCell<T> {
|
|
332
|
+
/// Creates a new, empty cell.
|
|
333
|
+
fn new() -> Self {
|
|
334
|
+
Self {
|
|
335
|
+
data: None,
|
|
336
|
+
fingerprint: 0,
|
|
337
|
+
accessed: false,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Marks the cell as not yet accessed in preparation of the next
|
|
342
|
+
/// compilation.
|
|
343
|
+
fn reset(&mut self) {
|
|
344
|
+
self.accessed = false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Gets the contents of the cell.
|
|
348
|
+
fn get(&self) -> Option<&FileResult<T>> {
|
|
349
|
+
self.data.as_ref()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// Gets the contents of the cell or initialize them.
|
|
353
|
+
fn get_or_init(
|
|
354
|
+
&mut self,
|
|
355
|
+
path: impl FnOnce() -> FileResult<PathBuf>,
|
|
356
|
+
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
|
|
357
|
+
) -> FileResult<T> {
|
|
358
|
+
// If we accessed the file already in this compilation, retrieve it.
|
|
359
|
+
if std::mem::replace(&mut self.accessed, true) {
|
|
360
|
+
if let Some(data) = &self.data {
|
|
361
|
+
return data.clone();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Read and hash the file.
|
|
366
|
+
let result = path().and_then(|p| read(&p));
|
|
367
|
+
let fingerprint = typst::utils::hash128(&result);
|
|
368
|
+
|
|
369
|
+
// If the file contents didn't change, yield the old processed data.
|
|
370
|
+
if std::mem::replace(&mut self.fingerprint, fingerprint) == fingerprint {
|
|
371
|
+
if let Some(data) = &self.data {
|
|
372
|
+
return data.clone();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let prev = self.data.take().and_then(Result::ok);
|
|
377
|
+
let value = result.and_then(|data| f(data, prev));
|
|
378
|
+
self.data = Some(value.clone());
|
|
379
|
+
|
|
380
|
+
value
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// Read a file.
|
|
385
|
+
fn read(path: &Path) -> FileResult<Vec<u8>> {
|
|
386
|
+
let f = |e| FileError::from_io(e, path);
|
|
387
|
+
if fs::metadata(path).map_err(f)?.is_dir() {
|
|
388
|
+
Err(FileError::IsDirectory)
|
|
389
|
+
} else {
|
|
390
|
+
fs::read(path).map_err(f)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// Decode UTF-8 with an optional BOM.
|
|
395
|
+
fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
|
|
396
|
+
// Remove UTF-8 BOM.
|
|
397
|
+
Ok(std::str::from_utf8(
|
|
398
|
+
buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf),
|
|
399
|
+
)?)
|
|
400
|
+
}
|
data/lib/base.rb
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
module Typst
|
|
2
|
+
class Base
|
|
3
|
+
attr_accessor :options
|
|
4
|
+
attr_accessor :compiled
|
|
5
|
+
|
|
6
|
+
def initialize(*options)
|
|
7
|
+
if options.size.zero?
|
|
8
|
+
raise "No options given"
|
|
9
|
+
elsif options.first.is_a?(String)
|
|
10
|
+
file, options = options
|
|
11
|
+
options ||= {}
|
|
12
|
+
options[:file] = file
|
|
13
|
+
elsif options.first.is_a?(Hash)
|
|
14
|
+
options = options.first
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if options.has_key?(:file)
|
|
18
|
+
raise "Can't find file" unless File.exist?(options[:file])
|
|
19
|
+
elsif options.has_key?(:body)
|
|
20
|
+
raise "Empty body" if options[:body].to_s.empty?
|
|
21
|
+
elsif options.has_key?(:zip)
|
|
22
|
+
raise "Can't find zip" unless File.exist?(options[:zip])
|
|
23
|
+
else
|
|
24
|
+
raise "No input given"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
root = Pathname.new(options[:root] || ".").expand_path
|
|
28
|
+
raise "Invalid path for root" unless root.exist?
|
|
29
|
+
options[:root] = root.to_s
|
|
30
|
+
|
|
31
|
+
font_paths = (options[:font_paths] || []).collect{ |fp| Pathname.new(fp).expand_path }
|
|
32
|
+
options[:font_paths] = font_paths.collect(&:to_s)
|
|
33
|
+
|
|
34
|
+
options[:dependencies] ||= {}
|
|
35
|
+
options[:fonts] ||= {}
|
|
36
|
+
options[:sys_inputs] ||= {}
|
|
37
|
+
options[:ignore_system_fonts] ||= false
|
|
38
|
+
options[:ignore_embedded_fonts] ||= false
|
|
39
|
+
|
|
40
|
+
self.options = options
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def typst_args
|
|
44
|
+
[options[:file], options[:root], options[:font_paths], options[:ignore_system_fonts], options[:ignore_embedded_fonts], options[:sys_inputs].map{ |k,v| [k.to_s,v.to_s] }.to_h]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def typst_pdf_args
|
|
48
|
+
options[:pdf_standards] ||= []
|
|
49
|
+
[*typst_args, options[:pdf_standards]]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def typst_png_args
|
|
53
|
+
[*typst_args, options[:ppi]]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.from_s(main_source, **options)
|
|
57
|
+
Typst::build_world_from_s(main_source, **options) do |opts|
|
|
58
|
+
from_options = options.merge(opts)
|
|
59
|
+
if from_options[:format]
|
|
60
|
+
Typst::formats[from_options[:format]].new(**from_options)
|
|
61
|
+
else
|
|
62
|
+
new(**from_options)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.from_zip(zip_file_path, main_file = "main.typ", **options)
|
|
68
|
+
Typst::build_world_from_zip(zip_file_path, main_file, **options) do |opts|
|
|
69
|
+
from_options = options.merge(opts)
|
|
70
|
+
if from_options[:format]
|
|
71
|
+
Typst::formats[from_options[:format]].new(**from_options)
|
|
72
|
+
else
|
|
73
|
+
new(**from_options)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def with_dependencies(dependencies)
|
|
79
|
+
self.options[:dependencies] = self.options[:dependencies].merge(dependencies)
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def with_fonts(fonts)
|
|
84
|
+
self.options[:fonts] = self.options[:fonts].merge(fonts)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def with_inputs(inputs)
|
|
89
|
+
self.options[:sys_inputs] = self.options[:sys_inputs].merge(inputs)
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def with_font_paths(font_paths)
|
|
94
|
+
self.options[:font_paths] = self.options[:font_paths] + font_paths
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def with_root(root)
|
|
99
|
+
self.options[:root] = root
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def compile(format, **options)
|
|
104
|
+
raise "Invalid format" if Typst::formats[format].nil?
|
|
105
|
+
|
|
106
|
+
options = self.options.merge(options)
|
|
107
|
+
|
|
108
|
+
if options.has_key?(:file)
|
|
109
|
+
Typst::formats[format].new(**options).compiled
|
|
110
|
+
elsif options.has_key?(:body)
|
|
111
|
+
Typst::build_world_from_s(self.options[:body], **options) do |opts|
|
|
112
|
+
Typst::formats[format].new(**options.merge(opts)).compiled
|
|
113
|
+
end
|
|
114
|
+
elsif options.has_key?(:zip)
|
|
115
|
+
main_file = options[:main_file]
|
|
116
|
+
Typst::build_world_from_zip(options[:zip], main_file, **options) do |opts|
|
|
117
|
+
Typst::formats[format].new(**options.merge(opts)).compiled
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
raise "No input given"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def query(selector, field: nil, one: false, format: "json")
|
|
125
|
+
query_options = { field: field, one: one, format: format }
|
|
126
|
+
|
|
127
|
+
if self.options.has_key?(:file)
|
|
128
|
+
Typst::Query.new(selector, self.options[:file], **query_options.merge(self.options.slice(:root, :font_paths, :ignore_system_fonts, :ignore_embedded_fonts, :sys_inputs)))
|
|
129
|
+
elsif self.options.has_key?(:body)
|
|
130
|
+
Typst::build_world_from_s(self.options[:body], **self.options) do |opts|
|
|
131
|
+
Typst::Query.new(selector, opts[:file], **query_options.merge(opts.slice(:root, :font_paths, :ignore_system_fonts, :ignore_embedded_fonts, :sys_inputs)))
|
|
132
|
+
end
|
|
133
|
+
elsif self.options.has_key?(:zip)
|
|
134
|
+
Typst::build_world_from_zip(self.options[:zip], **self.options) do |opts|
|
|
135
|
+
Typst::Query.new(selector, opts[:file], **query_options.merge(opts.slice(:root, :font_paths, :ignore_system_fonts, :ignore_embedded_fonts, :sys_inputs)))
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
raise "No input given"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|