typst 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []