typst 0.0.1

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.
@@ -0,0 +1,357 @@
1
+ use std::cell::{Cell, OnceCell, RefCell, RefMut};
2
+ use std::collections::HashMap;
3
+ use std::fs;
4
+ use std::hash::Hash;
5
+ use std::path::{Path, PathBuf};
6
+
7
+ use log::{debug};
8
+
9
+ use chrono::{DateTime, Datelike, Local};
10
+ use comemo::Prehashed;
11
+ use filetime::FileTime;
12
+ use same_file::Handle;
13
+ use siphasher::sip128::{Hasher128, SipHasher13};
14
+ use typst::diag::{FileError, FileResult, StrResult};
15
+ use typst::eval::{eco_format, Bytes, Datetime, Library};
16
+ use typst::font::{Font, FontBook, FontVariant};
17
+ use typst::syntax::{FileId, Source, VirtualPath};
18
+ use typst::World;
19
+
20
+ use crate::fonts::{FontSearcher, FontSlot};
21
+ use crate::package::prepare_package;
22
+
23
+ /// A world that provides access to the operating system.
24
+ pub struct SystemWorld {
25
+ /// The working directory.
26
+ workdir: Option<PathBuf>,
27
+ /// The canonical path to the input file.
28
+ input: PathBuf,
29
+ /// The root relative to which absolute paths are resolved.
30
+ root: PathBuf,
31
+ /// The input path.
32
+ main: FileId,
33
+ /// Typst's standard library.
34
+ library: Prehashed<Library>,
35
+ /// Metadata about discovered fonts.
36
+ book: Prehashed<FontBook>,
37
+ /// Locations of and storage for lazily loaded fonts.
38
+ fonts: Vec<FontSlot>,
39
+ /// Maps package-path combinations to canonical hashes. All package-path
40
+ /// combinations that point to the same file are mapped to the same hash. To
41
+ /// be used in conjunction with `paths`.
42
+ hashes: RefCell<HashMap<FileId, FileResult<PathHash>>>,
43
+ /// Maps canonical path hashes to source files and buffers.
44
+ slots: RefCell<HashMap<PathHash, PathSlot>>,
45
+ /// The current datetime if requested. This is stored here to ensure it is
46
+ /// always the same within one compilation. Reset between compilations.
47
+ now: OnceCell<DateTime<Local>>,
48
+ }
49
+
50
+ impl World for SystemWorld {
51
+ fn library(&self) -> &Prehashed<Library> {
52
+ &self.library
53
+ }
54
+
55
+ fn book(&self) -> &Prehashed<FontBook> {
56
+ &self.book
57
+ }
58
+
59
+ fn main(&self) -> Source {
60
+ self.source(self.main).unwrap()
61
+ }
62
+
63
+ fn source(&self, id: FileId) -> FileResult<Source> {
64
+ self.slot(id)?.source()
65
+ }
66
+
67
+ fn file(&self, id: FileId) -> FileResult<Bytes> {
68
+ self.slot(id)?.file()
69
+ }
70
+
71
+ fn font(&self, index: usize) -> Option<Font> {
72
+ self.fonts[index].get()
73
+ }
74
+
75
+ fn today(&self, offset: Option<i64>) -> Option<Datetime> {
76
+ let now = self.now.get_or_init(chrono::Local::now);
77
+
78
+ let naive = match offset {
79
+ None => now.naive_local(),
80
+ Some(o) => now.naive_utc() + chrono::Duration::hours(o),
81
+ };
82
+
83
+ Datetime::from_ymd(
84
+ naive.year(),
85
+ naive.month().try_into().ok()?,
86
+ naive.day().try_into().ok()?,
87
+ )
88
+ }
89
+ }
90
+
91
+ impl SystemWorld {
92
+ pub fn builder(root: PathBuf, main: PathBuf) -> SystemWorldBuilder {
93
+ SystemWorldBuilder::new(root, main)
94
+ }
95
+
96
+ /// Access the canonical slot for the given file id.
97
+ fn slot(&self, id: FileId) -> FileResult<RefMut<PathSlot>> {
98
+ let mut system_path = PathBuf::new();
99
+ let hash = self
100
+ .hashes
101
+ .borrow_mut()
102
+ .entry(id)
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
+ }))
125
+ }
126
+
127
+ /// The id of the main source file.
128
+ pub fn main(&self) -> FileId {
129
+ self.main
130
+ }
131
+
132
+ /// The root relative to which absolute paths are resolved.
133
+ pub fn root(&self) -> &Path {
134
+ &self.root
135
+ }
136
+
137
+ /// The current working directory.
138
+ pub fn workdir(&self) -> &Path {
139
+ self.workdir.as_deref().unwrap_or(Path::new("."))
140
+ }
141
+
142
+ /// Reset the compilation state in preparation of a new compilation.
143
+ pub fn reset(&mut self) {
144
+ self.hashes.borrow_mut().clear();
145
+ for slot in self.slots.borrow_mut().values_mut() {
146
+ slot.reset();
147
+ }
148
+ self.now.take();
149
+ }
150
+
151
+ /// Return the canonical path to the input file.
152
+ pub fn input(&self) -> &PathBuf {
153
+ &self.input
154
+ }
155
+
156
+ /// Lookup a source file by id.
157
+ #[track_caller]
158
+ pub fn lookup(&self, id: FileId) -> Source {
159
+ self.source(id)
160
+ .expect("file id does not point to any source file")
161
+ }
162
+ }
163
+
164
+ pub struct SystemWorldBuilder {
165
+ root: PathBuf,
166
+ main: PathBuf,
167
+ font_paths: Vec<PathBuf>,
168
+ font_files: Vec<PathBuf>,
169
+ }
170
+
171
+ impl SystemWorldBuilder {
172
+ pub fn new(root: PathBuf, main: PathBuf) -> Self {
173
+ Self {
174
+ root,
175
+ main,
176
+ font_paths: Vec::new(),
177
+ font_files: Vec::new(),
178
+ }
179
+ }
180
+
181
+ pub fn font_paths(mut self, font_paths: Vec<PathBuf>) -> Self {
182
+ self.font_paths = font_paths;
183
+ self
184
+ }
185
+
186
+ pub fn font_files(mut self, font_files: Vec<PathBuf>) -> Self {
187
+ self.font_files = font_files;
188
+ self
189
+ }
190
+
191
+ pub fn build(self) -> StrResult<SystemWorld> {
192
+ let mut searcher = FontSearcher::new();
193
+ searcher.search(&self.font_paths, &self.font_files);
194
+
195
+ for (name, infos) in searcher.book.families() {
196
+ for info in infos {
197
+ let FontVariant { style, weight, stretch } = info.variant;
198
+ debug!(target: "typst-rb", "font {name:?} variants - Style: {style:?}, Weight: {weight:?}, Stretch: {stretch:?}");
199
+ }
200
+ }
201
+
202
+ let input = self.main.canonicalize().map_err(|_| {
203
+ eco_format!("input file not found (searched at {})", self.main.display())
204
+ })?;
205
+ // Resolve the virtual path of the main file within the project root.
206
+ let main_path = VirtualPath::within_root(&self.main, &self.root)
207
+ .ok_or("input file must be contained in project root")?;
208
+
209
+ let world = SystemWorld {
210
+ workdir: std::env::current_dir().ok(),
211
+ input,
212
+ root: self.root,
213
+ main: FileId::new(None, main_path),
214
+ library: Prehashed::new(typst_library::build()),
215
+ book: Prehashed::new(searcher.book),
216
+ fonts: searcher.fonts,
217
+ hashes: RefCell::default(),
218
+ slots: RefCell::default(),
219
+ now: OnceCell::new(),
220
+ };
221
+ Ok(world)
222
+ }
223
+ }
224
+
225
+ /// Holds canonical data for all paths pointing to the same entity.
226
+ ///
227
+ /// Both fields can be populated if the file is both imported and read().
228
+ struct PathSlot {
229
+ /// The slot's canonical file id.
230
+ id: FileId,
231
+ /// The slot's path on the system.
232
+ path: PathBuf,
233
+ /// The lazily loaded and incrementally updated source file.
234
+ source: SlotCell<Source>,
235
+ /// The lazily loaded raw byte buffer.
236
+ file: SlotCell<Bytes>,
237
+ }
238
+
239
+ impl PathSlot {
240
+ /// Create a new path slot.
241
+ fn new(id: FileId, path: PathBuf) -> Self {
242
+ Self {
243
+ id,
244
+ path,
245
+ file: SlotCell::new(),
246
+ source: SlotCell::new(),
247
+ }
248
+ }
249
+
250
+ /// Marks the file as not yet accessed in preparation of the next
251
+ /// compilation.
252
+ fn reset(&self) {
253
+ self.source.reset();
254
+ self.file.reset();
255
+ }
256
+
257
+ fn source(&self) -> FileResult<Source> {
258
+ self.source.get_or_init(&self.path, |data, prev| {
259
+ let text = decode_utf8(&data)?;
260
+ if let Some(mut prev) = prev {
261
+ prev.replace(text);
262
+ Ok(prev)
263
+ } else {
264
+ Ok(Source::new(self.id, text.into()))
265
+ }
266
+ })
267
+ }
268
+
269
+ fn file(&self) -> FileResult<Bytes> {
270
+ self.file.get_or_init(&self.path, |data, _| Ok(data.into()))
271
+ }
272
+ }
273
+
274
+ /// Lazily processes data for a file.
275
+ struct SlotCell<T> {
276
+ data: RefCell<Option<FileResult<T>>>,
277
+ refreshed: Cell<FileTime>,
278
+ accessed: Cell<bool>,
279
+ }
280
+
281
+ impl<T: Clone> SlotCell<T> {
282
+ /// Creates a new, empty cell.
283
+ fn new() -> Self {
284
+ Self {
285
+ data: RefCell::new(None),
286
+ refreshed: Cell::new(FileTime::zero()),
287
+ accessed: Cell::new(false),
288
+ }
289
+ }
290
+
291
+ /// Marks the cell as not yet accessed in preparation of the next
292
+ /// compilation.
293
+ fn reset(&self) {
294
+ self.accessed.set(false);
295
+ }
296
+
297
+ /// Gets the contents of the cell or initialize them.
298
+ fn get_or_init(
299
+ &self,
300
+ path: &Path,
301
+ f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
302
+ ) -> FileResult<T> {
303
+ let mut borrow = self.data.borrow_mut();
304
+ if let Some(data) = &*borrow {
305
+ if self.accessed.replace(true) || self.current(path) {
306
+ return data.clone();
307
+ }
308
+ }
309
+
310
+ self.accessed.set(true);
311
+ self.refreshed.set(FileTime::now());
312
+ let prev = borrow.take().and_then(Result::ok);
313
+ let value = read(path).and_then(|data| f(data, prev));
314
+ *borrow = Some(value.clone());
315
+ value
316
+ }
317
+
318
+ /// Whether the cell contents are still up to date with the file system.
319
+ fn current(&self, path: &Path) -> bool {
320
+ fs::metadata(path).map_or(false, |meta| {
321
+ let modified = FileTime::from_last_modification_time(&meta);
322
+ modified < self.refreshed.get()
323
+ })
324
+ }
325
+ }
326
+
327
+ /// A hash that is the same for all paths pointing to the same entity.
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()))
338
+ }
339
+ }
340
+
341
+ /// Read a file.
342
+ fn read(path: &Path) -> FileResult<Vec<u8>> {
343
+ let f = |e| FileError::from_io(e, path);
344
+ if fs::metadata(path).map_err(f)?.is_dir() {
345
+ Err(FileError::IsDirectory)
346
+ } else {
347
+ fs::read(path).map_err(f)
348
+ }
349
+ }
350
+
351
+ /// Decode UTF-8 with an optional BOM.
352
+ fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
353
+ // Remove UTF-8 BOM.
354
+ Ok(std::str::from_utf8(
355
+ buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf),
356
+ )?)
357
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/lib/typst.rb ADDED
@@ -0,0 +1,115 @@
1
+ require_relative "typst/typst"
2
+ require "cgi"
3
+ require "pathname"
4
+ require "tmpdir"
5
+
6
+ module Typst
7
+ class Base
8
+ def self.from_s(main_source, dependencies: {}, fonts: {})
9
+ Dir.mktmpdir do |tmp_dir|
10
+ tmp_main_file = Pathname.new(tmp_dir).join("main.typ")
11
+ File.write(tmp_main_file, main_source)
12
+
13
+ dependencies.each do |dep_name, dep_source|
14
+ tmp_dep_file = Pathname.new(tmp_dir).join(dep_name)
15
+ File.write(tmp_dep_file, dep_source)
16
+ end
17
+
18
+ relative_font_path = Pathname.new(tmp_dir).join("fonts")
19
+ puts fonts
20
+ fonts.each do |font_name, font_bytes|
21
+ Pathname.new(relative_font_path).mkpath
22
+ tmp_font_file = relative_font_path.join(font_name)
23
+ File.write(tmp_font_file, font_bytes)
24
+ end
25
+
26
+ new(tmp_main_file, root: tmp_dir, font_paths: [relative_font_path])
27
+ end
28
+ end
29
+ end
30
+
31
+ class Pdf < Base
32
+ attr_accessor :input
33
+ attr_accessor :root
34
+ attr_accessor :font_paths
35
+ attr_accessor :bytes
36
+
37
+ def initialize(input, root: ".", font_paths: ["fonts"])
38
+ self.input = input
39
+ self.root = root
40
+ self.font_paths = font_paths
41
+
42
+ @bytes = Typst::_to_pdf(input, root, font_paths, File.dirname(__FILE__))
43
+ end
44
+
45
+ def document
46
+ bytes.pack("C*").to_s
47
+ end
48
+
49
+ def write(output)
50
+ File.open(output, "w"){ |f| f.write(document) }
51
+ end
52
+ end
53
+
54
+ class Svg < Base
55
+ attr_accessor :input
56
+ attr_accessor :root
57
+ attr_accessor :font_paths
58
+ attr_accessor :pages
59
+
60
+ def initialize(input, root: ".", font_paths: ["fonts"])
61
+ self.input = input
62
+ self.root = root
63
+ self.font_paths = font_paths
64
+
65
+ @pages = Typst::_to_svg(input, root, font_paths, File.dirname(__FILE__))
66
+ end
67
+
68
+ def write(output)
69
+ if pages.size > 1
70
+ pages.each_with_index do |page, i|
71
+ if output.include?("{{n}}")
72
+ file_name = output.gsub("{{n}}", (i+1).to_s)
73
+ else
74
+ file_name = File.basename(output, File.extname(output)) + "_" + i.to_s
75
+ file_name = file_name + File.extname(output)
76
+ end
77
+ File.open(file_name, "w"){ |f| f.write(page) }
78
+ end
79
+ elsif pages.size == 1
80
+ File.open(output, "w"){ |f| f.write(pages[0]) }
81
+ else
82
+ end
83
+ end
84
+ end
85
+
86
+ class Html < Base
87
+ attr_accessor :title
88
+ attr_accessor :svg
89
+ attr_accessor :html
90
+
91
+ def initialize(input, title = nil, root: ".", font_paths: ["fonts"])
92
+ title = title || File.basename(input, File.extname(input))
93
+ @title = CGI::escapeHTML(title)
94
+ @svg = Svg.new(input, root: root, font_paths: font_paths)
95
+ end
96
+
97
+ def markup
98
+ %{
99
+ <!DOCTYPE html>
100
+ <html>
101
+ <head>
102
+ <title>#{title}</title>
103
+ </head>
104
+ <body>
105
+ #{svg.pages.join("<br />")}
106
+ </body>
107
+ </html>
108
+ }
109
+ end
110
+
111
+ def write(output)
112
+ File.open(output, "w"){ |f| f.write(markup) }
113
+ end
114
+ end
115
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: typst
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Flinn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-11-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rb_sys
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.9.83
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.9.83
27
+ description:
28
+ email: flinn@actsasflinn.com
29
+ executables: []
30
+ extensions:
31
+ - ext/typst/extconf.rb
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Cargo.toml
35
+ - README.md
36
+ - README.typ
37
+ - Rakefile
38
+ - ext/typst/Cargo.lock
39
+ - ext/typst/Cargo.toml
40
+ - ext/typst/extconf.rb
41
+ - ext/typst/src/compiler.rs
42
+ - ext/typst/src/download.rs
43
+ - ext/typst/src/fonts.rs
44
+ - ext/typst/src/lib.rs
45
+ - ext/typst/src/package.rs
46
+ - ext/typst/src/world.rs
47
+ - lib/fonts/DejaVuSansMono-Bold.ttf
48
+ - lib/fonts/DejaVuSansMono-BoldOblique.ttf
49
+ - lib/fonts/DejaVuSansMono-Oblique.ttf
50
+ - lib/fonts/DejaVuSansMono.ttf
51
+ - lib/fonts/LinLibertine_R.ttf
52
+ - lib/fonts/LinLibertine_RB.ttf
53
+ - lib/fonts/LinLibertine_RBI.ttf
54
+ - lib/fonts/LinLibertine_RI.ttf
55
+ - lib/fonts/NewCM10-Bold.otf
56
+ - lib/fonts/NewCM10-BoldItalic.otf
57
+ - lib/fonts/NewCM10-Italic.otf
58
+ - lib/fonts/NewCM10-Regular.otf
59
+ - lib/fonts/NewCMMath-Book.otf
60
+ - lib/fonts/NewCMMath-Regular.otf
61
+ - lib/typst.rb
62
+ homepage: https://github.com/actsasflinn/typst-rb
63
+ licenses:
64
+ - Apache-2.0
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 3.2.2
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.4.22
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Ruby binding to typst, a new markup-based typesetting system that is powerful
85
+ and easy to learn.
86
+ test_files: []