typst 0.0.5 → 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 +61 -2
- metadata +4 -8
- data/ext/typst/src/lib.wip.rs +0 -203
data/ext/typst/src/world.rs
CHANGED
@@ -1,24 +1,23 @@
|
|
1
|
-
use std::cell::{Cell, OnceCell, RefCell, RefMut};
|
2
|
-
use std::collections::HashMap;
|
3
1
|
use std::fs;
|
4
|
-
use std::hash::Hash;
|
5
2
|
use std::path::{Path, PathBuf};
|
6
|
-
|
7
|
-
use log::{debug};
|
3
|
+
use std::sync::{Mutex, OnceLock};
|
8
4
|
|
9
5
|
use chrono::{DateTime, Datelike, Local};
|
10
|
-
use
|
11
|
-
use filetime::FileTime;
|
12
|
-
use same_file::Handle;
|
13
|
-
use siphasher::sip128::{Hasher128, SipHasher13};
|
6
|
+
use ecow::eco_format;
|
14
7
|
use typst::diag::{FileError, FileResult, StrResult};
|
15
|
-
use typst::
|
16
|
-
use typst::font::{Font, FontBook, FontVariant};
|
8
|
+
use typst::foundations::{Bytes, Datetime, Dict};
|
17
9
|
use typst::syntax::{FileId, Source, VirtualPath};
|
18
|
-
use typst::
|
10
|
+
use typst::text::{Font, FontBook};
|
11
|
+
use typst::utils::LazyHash;
|
12
|
+
use typst::{Features, Library, LibraryBuilder, World};
|
13
|
+
use typst_kit::{
|
14
|
+
fonts::{FontSearcher, FontSlot},
|
15
|
+
package::PackageStorage,
|
16
|
+
};
|
17
|
+
|
18
|
+
use std::collections::HashMap;
|
19
19
|
|
20
|
-
use crate::
|
21
|
-
use crate::package::prepare_package;
|
20
|
+
use crate::download::SlientDownload;
|
22
21
|
|
23
22
|
/// A world that provides access to the operating system.
|
24
23
|
pub struct SystemWorld {
|
@@ -31,41 +30,39 @@ pub struct SystemWorld {
|
|
31
30
|
/// The input path.
|
32
31
|
main: FileId,
|
33
32
|
/// Typst's standard library.
|
34
|
-
library:
|
33
|
+
library: LazyHash<Library>,
|
35
34
|
/// Metadata about discovered fonts.
|
36
|
-
book:
|
35
|
+
book: LazyHash<FontBook>,
|
37
36
|
/// Locations of and storage for lazily loaded fonts.
|
38
37
|
fonts: Vec<FontSlot>,
|
39
|
-
/// Maps
|
40
|
-
|
41
|
-
///
|
42
|
-
|
43
|
-
/// Maps canonical path hashes to source files and buffers.
|
44
|
-
slots: RefCell<HashMap<PathHash, PathSlot>>,
|
38
|
+
/// Maps file ids to source files and buffers.
|
39
|
+
slots: Mutex<HashMap<FileId, FileSlot>>,
|
40
|
+
/// Holds information about where packages are stored.
|
41
|
+
package_storage: PackageStorage,
|
45
42
|
/// The current datetime if requested. This is stored here to ensure it is
|
46
43
|
/// always the same within one compilation. Reset between compilations.
|
47
|
-
now:
|
44
|
+
now: OnceLock<DateTime<Local>>,
|
48
45
|
}
|
49
46
|
|
50
47
|
impl World for SystemWorld {
|
51
|
-
fn library(&self) -> &
|
48
|
+
fn library(&self) -> &LazyHash<Library> {
|
52
49
|
&self.library
|
53
50
|
}
|
54
51
|
|
55
|
-
fn book(&self) -> &
|
52
|
+
fn book(&self) -> &LazyHash<FontBook> {
|
56
53
|
&self.book
|
57
54
|
}
|
58
55
|
|
59
|
-
fn main(&self) ->
|
60
|
-
self.
|
56
|
+
fn main(&self) -> FileId {
|
57
|
+
self.main
|
61
58
|
}
|
62
59
|
|
63
60
|
fn source(&self, id: FileId) -> FileResult<Source> {
|
64
|
-
self.slot(id
|
61
|
+
self.slot(id, |slot| slot.source(&self.root, &self.package_storage))
|
65
62
|
}
|
66
63
|
|
67
64
|
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
68
|
-
self.slot(id
|
65
|
+
self.slot(id, |slot| slot.file(&self.root, &self.package_storage))
|
69
66
|
}
|
70
67
|
|
71
68
|
fn font(&self, index: usize) -> Option<Font> {
|
@@ -94,34 +91,12 @@ impl SystemWorld {
|
|
94
91
|
}
|
95
92
|
|
96
93
|
/// Access the canonical slot for the given file id.
|
97
|
-
fn slot(&self, id: FileId) ->
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
.or_insert_with(|| {
|
104
|
-
// Determine the root path relative to which the file path
|
105
|
-
// will be resolved.
|
106
|
-
let buf;
|
107
|
-
let mut root = &self.root;
|
108
|
-
if let Some(spec) = id.package() {
|
109
|
-
buf = prepare_package(spec)?;
|
110
|
-
root = &buf;
|
111
|
-
}
|
112
|
-
// Join the path to the root. If it tries to escape, deny
|
113
|
-
// access. Note: It can still escape via symlinks.
|
114
|
-
system_path = id.vpath().resolve(root).ok_or(FileError::AccessDenied)?;
|
115
|
-
|
116
|
-
PathHash::new(&system_path)
|
117
|
-
})
|
118
|
-
.clone()?;
|
119
|
-
|
120
|
-
Ok(RefMut::map(self.slots.borrow_mut(), |paths| {
|
121
|
-
paths
|
122
|
-
.entry(hash)
|
123
|
-
.or_insert_with(|| PathSlot::new(id, system_path))
|
124
|
-
}))
|
94
|
+
fn slot<F, T>(&self, id: FileId, f: F) -> T
|
95
|
+
where
|
96
|
+
F: FnOnce(&mut FileSlot) -> T,
|
97
|
+
{
|
98
|
+
let mut map = self.slots.lock().unwrap();
|
99
|
+
f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
|
125
100
|
}
|
126
101
|
|
127
102
|
/// The id of the main source file.
|
@@ -141,8 +116,7 @@ impl SystemWorld {
|
|
141
116
|
|
142
117
|
/// Reset the compilation state in preparation of a new compilation.
|
143
118
|
pub fn reset(&mut self) {
|
144
|
-
self.
|
145
|
-
for slot in self.slots.borrow_mut().values_mut() {
|
119
|
+
for slot in self.slots.lock().unwrap().values_mut() {
|
146
120
|
slot.reset();
|
147
121
|
}
|
148
122
|
self.now.take();
|
@@ -165,7 +139,9 @@ pub struct SystemWorldBuilder {
|
|
165
139
|
root: PathBuf,
|
166
140
|
main: PathBuf,
|
167
141
|
font_paths: Vec<PathBuf>,
|
168
|
-
|
142
|
+
ignore_system_fonts: bool,
|
143
|
+
inputs: Dict,
|
144
|
+
features: Features,
|
169
145
|
}
|
170
146
|
|
171
147
|
impl SystemWorldBuilder {
|
@@ -174,7 +150,9 @@ impl SystemWorldBuilder {
|
|
174
150
|
root,
|
175
151
|
main,
|
176
152
|
font_paths: Vec::new(),
|
177
|
-
|
153
|
+
ignore_system_fonts: false,
|
154
|
+
inputs: Dict::default(),
|
155
|
+
features: Features::default(),
|
178
156
|
}
|
179
157
|
}
|
180
158
|
|
@@ -183,21 +161,25 @@ impl SystemWorldBuilder {
|
|
183
161
|
self
|
184
162
|
}
|
185
163
|
|
186
|
-
pub fn
|
187
|
-
self.
|
164
|
+
pub fn ignore_system_fonts(mut self, ignore: bool) -> Self {
|
165
|
+
self.ignore_system_fonts = ignore;
|
188
166
|
self
|
189
167
|
}
|
190
168
|
|
191
|
-
pub fn
|
192
|
-
|
193
|
-
|
169
|
+
pub fn inputs(mut self, inputs: Dict) -> Self {
|
170
|
+
self.inputs = inputs;
|
171
|
+
self
|
172
|
+
}
|
194
173
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
174
|
+
pub fn features(mut self, features: Features) -> Self {
|
175
|
+
self.features = features;
|
176
|
+
self
|
177
|
+
}
|
178
|
+
|
179
|
+
pub fn build(self) -> StrResult<SystemWorld> {
|
180
|
+
let fonts = FontSearcher::new()
|
181
|
+
.include_system_fonts(!self.ignore_system_fonts)
|
182
|
+
.search_with(&self.font_paths);
|
201
183
|
|
202
184
|
let input = self.main.canonicalize().map_err(|_| {
|
203
185
|
eco_format!("input file not found (searched at {})", self.main.display())
|
@@ -211,12 +193,12 @@ impl SystemWorldBuilder {
|
|
211
193
|
input,
|
212
194
|
root: self.root,
|
213
195
|
main: FileId::new(None, main_path),
|
214
|
-
library:
|
215
|
-
book:
|
216
|
-
fonts:
|
217
|
-
|
218
|
-
|
219
|
-
now:
|
196
|
+
library: LazyHash::new(LibraryBuilder::default().with_inputs(self.inputs).with_features(self.features).build()),
|
197
|
+
book: LazyHash::new(fonts.book),
|
198
|
+
fonts: fonts.fonts,
|
199
|
+
slots: Mutex::default(),
|
200
|
+
package_storage: PackageStorage::new(None, None, crate::download::downloader()),
|
201
|
+
now: OnceLock::new(),
|
220
202
|
};
|
221
203
|
Ok(world)
|
222
204
|
}
|
@@ -225,23 +207,20 @@ impl SystemWorldBuilder {
|
|
225
207
|
/// Holds canonical data for all paths pointing to the same entity.
|
226
208
|
///
|
227
209
|
/// Both fields can be populated if the file is both imported and read().
|
228
|
-
struct
|
210
|
+
struct FileSlot {
|
229
211
|
/// The slot's canonical file id.
|
230
212
|
id: FileId,
|
231
|
-
/// The slot's path on the system.
|
232
|
-
path: PathBuf,
|
233
213
|
/// The lazily loaded and incrementally updated source file.
|
234
214
|
source: SlotCell<Source>,
|
235
215
|
/// The lazily loaded raw byte buffer.
|
236
216
|
file: SlotCell<Bytes>,
|
237
217
|
}
|
238
218
|
|
239
|
-
impl
|
219
|
+
impl FileSlot {
|
240
220
|
/// Create a new path slot.
|
241
|
-
fn new(id: FileId
|
221
|
+
fn new(id: FileId) -> Self {
|
242
222
|
Self {
|
243
223
|
id,
|
244
|
-
path,
|
245
224
|
file: SlotCell::new(),
|
246
225
|
source: SlotCell::new(),
|
247
226
|
}
|
@@ -249,92 +228,107 @@ impl PathSlot {
|
|
249
228
|
|
250
229
|
/// Marks the file as not yet accessed in preparation of the next
|
251
230
|
/// compilation.
|
252
|
-
fn reset(&self) {
|
231
|
+
fn reset(&mut self) {
|
253
232
|
self.source.reset();
|
254
233
|
self.file.reset();
|
255
234
|
}
|
256
235
|
|
257
|
-
fn source(&self) -> FileResult<Source> {
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
236
|
+
fn source(&mut self, root: &Path, package_storage: &PackageStorage) -> FileResult<Source> {
|
237
|
+
let id = self.id;
|
238
|
+
self.source.get_or_init(
|
239
|
+
|| system_path(root, id, package_storage),
|
240
|
+
|data, prev| {
|
241
|
+
let text = decode_utf8(&data)?;
|
242
|
+
if let Some(mut prev) = prev {
|
243
|
+
prev.replace(text);
|
244
|
+
Ok(prev)
|
245
|
+
} else {
|
246
|
+
Ok(Source::new(self.id, text.into()))
|
247
|
+
}
|
248
|
+
},
|
249
|
+
)
|
250
|
+
}
|
251
|
+
|
252
|
+
fn file(&mut self, root: &Path, package_storage: &PackageStorage) -> FileResult<Bytes> {
|
253
|
+
let id = self.id;
|
254
|
+
self.file.get_or_init(
|
255
|
+
|| system_path(root, id, package_storage),
|
256
|
+
|data, _| Ok(Bytes::new(data)),
|
257
|
+
)
|
267
258
|
}
|
259
|
+
}
|
268
260
|
|
269
|
-
|
270
|
-
|
261
|
+
/// The path of the slot on the system.
|
262
|
+
fn system_path(root: &Path, id: FileId, package_storage: &PackageStorage) -> FileResult<PathBuf> {
|
263
|
+
// Determine the root path relative to which the file path
|
264
|
+
// will be resolved.
|
265
|
+
let buf;
|
266
|
+
let mut root = root;
|
267
|
+
if let Some(spec) = id.package() {
|
268
|
+
buf = package_storage.prepare_package(spec, &mut SlientDownload(&spec))?;
|
269
|
+
root = &buf;
|
271
270
|
}
|
271
|
+
|
272
|
+
// Join the path to the root. If it tries to escape, deny
|
273
|
+
// access. Note: It can still escape via symlinks.
|
274
|
+
id.vpath().resolve(root).ok_or(FileError::AccessDenied)
|
272
275
|
}
|
273
276
|
|
274
277
|
/// Lazily processes data for a file.
|
275
278
|
struct SlotCell<T> {
|
276
|
-
data
|
277
|
-
|
278
|
-
|
279
|
+
/// The processed data.
|
280
|
+
data: Option<FileResult<T>>,
|
281
|
+
/// A hash of the raw file contents / access error.
|
282
|
+
fingerprint: u128,
|
283
|
+
/// Whether the slot has been accessed in the current compilation.
|
284
|
+
accessed: bool,
|
279
285
|
}
|
280
286
|
|
281
287
|
impl<T: Clone> SlotCell<T> {
|
282
288
|
/// Creates a new, empty cell.
|
283
289
|
fn new() -> Self {
|
284
290
|
Self {
|
285
|
-
data:
|
286
|
-
|
287
|
-
accessed:
|
291
|
+
data: None,
|
292
|
+
fingerprint: 0,
|
293
|
+
accessed: false,
|
288
294
|
}
|
289
295
|
}
|
290
296
|
|
291
297
|
/// Marks the cell as not yet accessed in preparation of the next
|
292
298
|
/// compilation.
|
293
|
-
fn reset(&self) {
|
294
|
-
self.accessed
|
299
|
+
fn reset(&mut self) {
|
300
|
+
self.accessed = false;
|
295
301
|
}
|
296
302
|
|
297
303
|
/// Gets the contents of the cell or initialize them.
|
298
304
|
fn get_or_init(
|
299
|
-
&self,
|
300
|
-
path:
|
305
|
+
&mut self,
|
306
|
+
path: impl FnOnce() -> FileResult<PathBuf>,
|
301
307
|
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
|
302
308
|
) -> FileResult<T> {
|
303
|
-
|
304
|
-
if
|
305
|
-
if
|
309
|
+
// If we accessed the file already in this compilation, retrieve it.
|
310
|
+
if std::mem::replace(&mut self.accessed, true) {
|
311
|
+
if let Some(data) = &self.data {
|
306
312
|
return data.clone();
|
307
313
|
}
|
308
314
|
}
|
309
315
|
|
310
|
-
|
311
|
-
|
312
|
-
let
|
313
|
-
let value = read(path).and_then(|data| f(data, prev));
|
314
|
-
*borrow = Some(value.clone());
|
315
|
-
value
|
316
|
-
}
|
316
|
+
// Read and hash the file.
|
317
|
+
let result = path().and_then(|p| read(&p));
|
318
|
+
let fingerprint = typst::utils::hash128(&result);
|
317
319
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
}
|
324
|
-
|
325
|
-
|
320
|
+
// If the file contents didn't change, yield the old processed data.
|
321
|
+
if std::mem::replace(&mut self.fingerprint, fingerprint) == fingerprint {
|
322
|
+
if let Some(data) = &self.data {
|
323
|
+
return data.clone();
|
324
|
+
}
|
325
|
+
}
|
326
|
+
|
327
|
+
let prev = self.data.take().and_then(Result::ok);
|
328
|
+
let value = result.and_then(|data| f(data, prev));
|
329
|
+
self.data = Some(value.clone());
|
326
330
|
|
327
|
-
|
328
|
-
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
329
|
-
struct PathHash(u128);
|
330
|
-
|
331
|
-
impl PathHash {
|
332
|
-
fn new(path: &Path) -> FileResult<Self> {
|
333
|
-
let f = |e| FileError::from_io(e, path);
|
334
|
-
let handle = Handle::from_path(path).map_err(f)?;
|
335
|
-
let mut state = SipHasher13::new();
|
336
|
-
handle.hash(&mut state);
|
337
|
-
Ok(Self(state.finish128().as_u128()))
|
331
|
+
value
|
338
332
|
}
|
339
333
|
}
|
340
334
|
|
data/lib/typst.rb
CHANGED
@@ -78,7 +78,7 @@ module Typst
|
|
78
78
|
|
79
79
|
def initialize(input, root: ".", font_paths: [])
|
80
80
|
super(input, root: root, font_paths: font_paths)
|
81
|
-
@bytes = Typst::_to_pdf(self.input, self.root, self.font_paths, File.dirname(__FILE__))
|
81
|
+
@bytes = Typst::_to_pdf(self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {})[0]
|
82
82
|
end
|
83
83
|
|
84
84
|
def document
|
@@ -91,7 +91,33 @@ module Typst
|
|
91
91
|
|
92
92
|
def initialize(input, root: ".", font_paths: [])
|
93
93
|
super(input, root: root, font_paths: font_paths)
|
94
|
-
@pages = Typst::_to_svg(self.input, self.root, self.font_paths, File.dirname(__FILE__))
|
94
|
+
@pages = Typst::_to_svg(self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {}).collect{ |page| page.pack("C*").to_s }
|
95
|
+
end
|
96
|
+
|
97
|
+
def write(output)
|
98
|
+
if pages.size > 1
|
99
|
+
pages.each_with_index do |page, i|
|
100
|
+
if output.include?("{{n}}")
|
101
|
+
file_name = output.gsub("{{n}}", (i+1).to_s)
|
102
|
+
else
|
103
|
+
file_name = File.basename(output, File.extname(output)) + "_" + i.to_s
|
104
|
+
file_name = file_name + File.extname(output)
|
105
|
+
end
|
106
|
+
File.open(file_name, "w"){ |f| f.write(page) }
|
107
|
+
end
|
108
|
+
elsif pages.size == 1
|
109
|
+
File.open(output, "w"){ |f| f.write(pages[0]) }
|
110
|
+
else
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Png < Base
|
116
|
+
attr_accessor :pages
|
117
|
+
|
118
|
+
def initialize(input, root: ".", font_paths: [])
|
119
|
+
super(input, root: root, font_paths: font_paths)
|
120
|
+
@pages = Typst::_to_png(self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {}).collect{ |page| page.pack("C*").to_s }
|
95
121
|
end
|
96
122
|
|
97
123
|
def write(output)
|
@@ -139,4 +165,37 @@ module Typst
|
|
139
165
|
end
|
140
166
|
alias_method :document, :markup
|
141
167
|
end
|
168
|
+
|
169
|
+
class HtmlExperimental < Base
|
170
|
+
attr_accessor :bytes
|
171
|
+
|
172
|
+
def initialize(input, root: ".", font_paths: [])
|
173
|
+
super(input, root: root, font_paths: font_paths)
|
174
|
+
@bytes = Typst::_to_html(self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {})[0]
|
175
|
+
end
|
176
|
+
|
177
|
+
def document
|
178
|
+
bytes.pack("C*").to_s
|
179
|
+
end
|
180
|
+
alias_method :markup, :document
|
181
|
+
end
|
182
|
+
|
183
|
+
class Query < Base
|
184
|
+
attr_accessor :format
|
185
|
+
attr_accessor :result
|
186
|
+
|
187
|
+
def initialize(selector, input, field: nil, one: false, format: "json", root: ".", font_paths: [])
|
188
|
+
super(input, root: root, font_paths: font_paths)
|
189
|
+
self.format = format
|
190
|
+
self.result = Typst::_query(selector, field, one, format, self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {})
|
191
|
+
end
|
192
|
+
|
193
|
+
def result(raw: false)
|
194
|
+
case raw || format
|
195
|
+
when "json" then JSON(@result)
|
196
|
+
when "yaml" then YAML::safe_load(@result)
|
197
|
+
else @result
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
142
201
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typst
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Flinn
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-02-23 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rb_sys
|
@@ -24,7 +23,6 @@ dependencies:
|
|
24
23
|
- - ">="
|
25
24
|
- !ruby/object:Gem::Version
|
26
25
|
version: 0.9.83
|
27
|
-
description:
|
28
26
|
email: flinn@actsasflinn.com
|
29
27
|
executables: []
|
30
28
|
extensions:
|
@@ -42,8 +40,8 @@ files:
|
|
42
40
|
- ext/typst/src/download.rs
|
43
41
|
- ext/typst/src/fonts.rs
|
44
42
|
- ext/typst/src/lib.rs
|
45
|
-
- ext/typst/src/lib.wip.rs
|
46
43
|
- ext/typst/src/package.rs
|
44
|
+
- ext/typst/src/query.rs
|
47
45
|
- ext/typst/src/world.rs
|
48
46
|
- lib/fonts/DejaVuSansMono-Bold.ttf
|
49
47
|
- lib/fonts/DejaVuSansMono-BoldOblique.ttf
|
@@ -64,7 +62,6 @@ homepage: https://github.com/actsasflinn/typst-rb
|
|
64
62
|
licenses:
|
65
63
|
- Apache-2.0
|
66
64
|
metadata: {}
|
67
|
-
post_install_message:
|
68
65
|
rdoc_options: []
|
69
66
|
require_paths:
|
70
67
|
- lib
|
@@ -79,8 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
76
|
- !ruby/object:Gem::Version
|
80
77
|
version: '0'
|
81
78
|
requirements: []
|
82
|
-
rubygems_version: 3.
|
83
|
-
signing_key:
|
79
|
+
rubygems_version: 3.6.3
|
84
80
|
specification_version: 4
|
85
81
|
summary: Ruby binding to typst, a new markup-based typesetting system that is powerful
|
86
82
|
and easy to learn.
|