typst-rails 0.1.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 +7 -0
- data/CHANGELOG.md +68 -0
- data/MIT-LICENSE +20 -0
- data/README.md +264 -0
- data/lib/tasks/typst_rails/tasks.rake +6 -0
- data/lib/typst_rails/backends/base.rb +30 -0
- data/lib/typst_rails/backends/cli.rb +90 -0
- data/lib/typst_rails/backends/gem.rb +43 -0
- data/lib/typst_rails/backends/registry.rb +82 -0
- data/lib/typst_rails/backends.rb +15 -0
- data/lib/typst_rails/framework_detection.rb +26 -0
- data/lib/typst_rails/handler.rb +75 -0
- data/lib/typst_rails/helpers.rb +354 -0
- data/lib/typst_rails/rage_integration.rb +18 -0
- data/lib/typst_rails/railtie.rb +18 -0
- data/lib/typst_rails/renderer.rb +386 -0
- data/lib/typst_rails/sinatra_integration.rb +33 -0
- data/lib/typst_rails/version.rb +5 -0
- data/lib/typst_rails.rb +146 -0
- metadata +225 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3ffc46a6af771ebebfbe1754e08bc1e113cd0dd8bb3f2cc2f7ec50d364e7ece8
|
|
4
|
+
data.tar.gz: 393e6e688947174901bf6acb044d884462122b33e02c700b4d5cbed79699fc97
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e55317c039f546f59937c1026cfda8597df9908a481cc5783c7e8607226f7d79b80858befddb7594c1b223a00f0d73fb7d8f9765193e45a8786bb1054386a858
|
|
7
|
+
data.tar.gz: f310751f2756d97a75dba555046a2f54f6be863e5e8531abdb82007a4700e896a1beffb7bc71092680a538dd5e000d852a4da7771df4d3cb8033e43c79bfd855
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Swappable Typst compilation backends: a `:cli` backend (shells out to the
|
|
12
|
+
Typst executable, the original behavior) and a `:gem` backend (uses the
|
|
13
|
+
`typst` RubyGem to compile in-process when installed), selected
|
|
14
|
+
automatically via `TypstRails::Backends::Registry` or set explicitly with
|
|
15
|
+
`TypstRails.configure { |c| c.backend = :gem }`. Custom backends can be
|
|
16
|
+
registered for other compilation strategies.
|
|
17
|
+
- Docker-based end-to-end tests (`e2e-docker/`, `rake e2e:docker`) covering
|
|
18
|
+
the backend auto-detection matrix (CLI-only, gem-only, both, neither) and a
|
|
19
|
+
fresh `gem build` + `gem install` smoke test, independent of the local dev
|
|
20
|
+
environment.
|
|
21
|
+
- Comprehensive YARD documentation for all public APIs
|
|
22
|
+
- Enhanced gemspec metadata with bug tracker and documentation URIs
|
|
23
|
+
- MFA requirement for RubyGems publishing
|
|
24
|
+
- CHANGELOG.md following Keep a Changelog format
|
|
25
|
+
- SECURITY.md with security reporting process
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Improved module and class documentation with examples
|
|
29
|
+
- Enhanced gemspec description with clearer value proposition
|
|
30
|
+
- Updated author email to commercial@durableprogramming.com
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- `Backends::Cli#available?` raised `NoMethodError` when the gem was loaded
|
|
34
|
+
via a bare `require "typst_rails/renderer"`, because `executable_path` read
|
|
35
|
+
`TypstRails.configuration` without checking that the top-level module was
|
|
36
|
+
loaded. Safe navigation did not help: the method itself was undefined. The
|
|
37
|
+
fault was masked wherever the `typst` gem is installed, since the `:gem`
|
|
38
|
+
backend is selected first and `Cli#available?` is never reached.
|
|
39
|
+
- `TypstRails.configuration.typst_executable_path` is now actually used by
|
|
40
|
+
the CLI backend when compiling (previously ignored, so the setting had no
|
|
41
|
+
effect).
|
|
42
|
+
|
|
43
|
+
## [0.1.0] - 2024-01-XX (Initial Release)
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- Core Typst rendering functionality via `TypstRails::Renderer`
|
|
47
|
+
- Automatic framework detection for Rails, Rage, and Sinatra
|
|
48
|
+
- Rails integration with `.typ` template handler
|
|
49
|
+
- Sinatra helper methods for Typst rendering
|
|
50
|
+
- Rage framework integration
|
|
51
|
+
- Standalone Ruby support (no framework required)
|
|
52
|
+
- Helper methods for text escaping, HTML/Markdown conversion, and URL encoding
|
|
53
|
+
- Comprehensive test suite with unit and E2E tests
|
|
54
|
+
- Code coverage reporting with SimpleCov
|
|
55
|
+
- RuboCop configuration for code quality
|
|
56
|
+
- Support for ERB preprocessing in Typst templates
|
|
57
|
+
- Automatic data binding from Rails view contexts
|
|
58
|
+
- JSON-based data passing to Typst templates
|
|
59
|
+
- Error handling with detailed error messages
|
|
60
|
+
- Temporary file management with automatic cleanup
|
|
61
|
+
|
|
62
|
+
### Security
|
|
63
|
+
- Input validation for template source size (10MB limit)
|
|
64
|
+
- Safe handling of temporary files with proper cleanup
|
|
65
|
+
- Argument validation for all public methods
|
|
66
|
+
|
|
67
|
+
[Unreleased]: https://github.com/durable-oss/typst-rails/compare/v0.1.0...HEAD
|
|
68
|
+
[0.1.0]: https://github.com/durable-oss/typst-rails/releases/tag/v0.1.0
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Durable Programming Team
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Typst Rails
|
|
2
|
+
|
|
3
|
+
[](https://github.com/durable-oss/typst-rails/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/typst-rails)
|
|
5
|
+
[](MIT-LICENSE)
|
|
6
|
+
|
|
7
|
+
`TypstRails` provides helpers for using [Typst](https://typst.app/) typesetting system with Ruby applications. Generate high-quality PDFs from Typst templates with seamless framework integration for Rails, Rage, and Sinatra. TypstRails can be used with either the typst gem or with a typst CLI.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add this line to your application's Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'typst-rails'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
And then execute:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
$ bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install it yourself as:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
$ gem install typst-rails
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
# Rails Usage
|
|
31
|
+
|
|
32
|
+
`.typ` templates are automatically registered and work just like ERB templates:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# app/controllers/reports_controller.rb
|
|
36
|
+
class ReportsController < ApplicationController
|
|
37
|
+
def monthly
|
|
38
|
+
@title = "Monthly Report"
|
|
39
|
+
@author = "Durable Systems Inc."
|
|
40
|
+
|
|
41
|
+
respond_to do |format|
|
|
42
|
+
format.pdf { render template: "reports/monthly" }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```typst
|
|
49
|
+
<%# app/views/reports/monthly.typ %>
|
|
50
|
+
<% data = { title: @title, author: @author } %>
|
|
51
|
+
#let data = json("typst_data.json")
|
|
52
|
+
|
|
53
|
+
= #data.title
|
|
54
|
+
|
|
55
|
+
Author: #data.author
|
|
56
|
+
|
|
57
|
+
Generated on <%= Time.now.strftime("%Y-%m-%d") %>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Rage Integration
|
|
61
|
+
|
|
62
|
+
Rage support is automatically enabled when the Rage framework is detected. Template integration follows Rage's conventions.
|
|
63
|
+
|
|
64
|
+
### Sinatra Integration
|
|
65
|
+
|
|
66
|
+
When Sinatra is detected, a `typst` helper method is available:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
require 'sinatra'
|
|
70
|
+
require 'typst_rails'
|
|
71
|
+
|
|
72
|
+
get '/report.pdf' do
|
|
73
|
+
typst 'templates/report.typ', {
|
|
74
|
+
title: "Monthly Report",
|
|
75
|
+
author: "Durable Systems Inc."
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Helpers
|
|
81
|
+
|
|
82
|
+
The gem provides several helper methods for working with Typst templates:
|
|
83
|
+
|
|
84
|
+
#### Text Escaping
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
escape_typst("Price: $100") # => "Price: \\$100"
|
|
88
|
+
escape_typst("Hello #world") # => "Hello \\#world"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### HTML to Markdown/Typst Conversion
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# Convert HTML to Markdown
|
|
95
|
+
html_to_markdown("<h1>Title</h1><p>Content</p>")
|
|
96
|
+
# => "# Title\n\nContent"
|
|
97
|
+
|
|
98
|
+
# Convert HTML directly to Typst
|
|
99
|
+
html_to_typst("<h1>Title</h1>")
|
|
100
|
+
# => "= Title\n\n"
|
|
101
|
+
|
|
102
|
+
# Sanitize HTML before conversion
|
|
103
|
+
sanitize_html('<script>alert("xss")</script><p>Safe</p>')
|
|
104
|
+
# => "<p>Safe</p>"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Markdown Conversion
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Convert Markdown to Typst syntax
|
|
111
|
+
markdown_to_typst("# Title\n## Subtitle")
|
|
112
|
+
# => "= Title\n== Subtitle"
|
|
113
|
+
|
|
114
|
+
# Include external Markdown files
|
|
115
|
+
include_markdown("./content.md")
|
|
116
|
+
# Reads and converts the file to Typst syntax
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### URL Encoding
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
url_encode("hello world") # => "hello+world"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
These helpers are available in:
|
|
126
|
+
- ERB templates (Rails) - use directly: `<%= escape_typst(@text) %>`
|
|
127
|
+
- Plain Ruby code - include `TypstRails::Helpers` module
|
|
128
|
+
- The renderer includes these helpers automatically
|
|
129
|
+
|
|
130
|
+
### Configuration
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
TypstRails.configure do |config|
|
|
134
|
+
config.typst_executable_path = "/custom/path/to/typst"
|
|
135
|
+
config.default_root_path = Rails.root.join("app", "assets", "typst")
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Compilation Backends
|
|
140
|
+
|
|
141
|
+
`TypstRails` compiles documents through a pluggable backend. Two backends ship
|
|
142
|
+
with the gem:
|
|
143
|
+
|
|
144
|
+
- **`:cli`** shells out to the `typst` executable. This is the original
|
|
145
|
+
approach and requires Typst to be [installed separately](https://typst.app/docs/tutorial/setup/)
|
|
146
|
+
and available on `PATH`.
|
|
147
|
+
- **`:gem`** uses the [`typst`](https://rubygems.org/gems/typst) RubyGem, a
|
|
148
|
+
native extension that compiles in-process—no subprocess or separate Typst
|
|
149
|
+
install required. Add it to your Gemfile to enable it:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
gem "typst"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
By default (`config.backend = :auto`), TypstRails prefers the `typst` gem
|
|
156
|
+
when it's installed and falls back to the CLI otherwise. Force a specific
|
|
157
|
+
backend if you need to:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
TypstRails.configure do |config|
|
|
161
|
+
config.backend = :gem # or :cli
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
You can also register your own backend—for example, to compile against a
|
|
166
|
+
remote Typst service:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class MyRemoteBackend < TypstRails::Backends::Base
|
|
170
|
+
def available?
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def compile(typ_path, root_dir)
|
|
175
|
+
# ... return PDF bytes, or raise TypstRails::Error on failure
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
TypstRails::Backends::Registry.register(:my_remote, MyRemoteBackend.new)
|
|
180
|
+
TypstRails.configure { |config| config.backend = :my_remote }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Testing
|
|
184
|
+
|
|
185
|
+
The gem includes comprehensive testing at multiple levels:
|
|
186
|
+
|
|
187
|
+
### Unit Tests
|
|
188
|
+
|
|
189
|
+
Run the unit test suite:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
bundle exec rake test
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Tests cover:
|
|
196
|
+
- Framework detection (Rails, Rage, Sinatra)
|
|
197
|
+
- Core renderer functionality
|
|
198
|
+
- Error handling and edge cases
|
|
199
|
+
- All helper methods
|
|
200
|
+
- Input validation and defensive programming
|
|
201
|
+
|
|
202
|
+
### End-to-End Tests
|
|
203
|
+
|
|
204
|
+
**Requires Typst to be installed.**
|
|
205
|
+
|
|
206
|
+
The E2E tests process real ERB templates using all helpers and compile them with Typst to generate actual PDFs:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Run E2E tests
|
|
210
|
+
bundle exec rake e2e
|
|
211
|
+
|
|
212
|
+
# Run all tests (unit + E2E)
|
|
213
|
+
bundle exec rake test_all
|
|
214
|
+
|
|
215
|
+
# Clean up generated test outputs
|
|
216
|
+
bundle exec rake clean_e2e
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
E2E tests verify:
|
|
220
|
+
- Text escaping with real Typst special characters
|
|
221
|
+
- HTML to Typst conversion with actual compilation
|
|
222
|
+
- Markdown to Typst syntax transformation
|
|
223
|
+
- External Markdown file inclusion
|
|
224
|
+
- Complex real-world templates with multiple helpers
|
|
225
|
+
|
|
226
|
+
See [e2e-tests/README.md](e2e-tests/README.md) for detailed documentation.
|
|
227
|
+
|
|
228
|
+
### Docker End-to-End Tests
|
|
229
|
+
|
|
230
|
+
**Requires Docker.**
|
|
231
|
+
|
|
232
|
+
Verifies backend auto-detection (`:cli` vs `:gem`) across isolated container
|
|
233
|
+
environments, and that the gem works correctly when built and installed like
|
|
234
|
+
a real release rather than loaded from the working tree:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
bundle exec rake e2e:docker
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
See [e2e-docker/README.md](e2e-docker/README.md) for detailed documentation.
|
|
241
|
+
|
|
242
|
+
### Code Coverage
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
bundle exec rake test
|
|
246
|
+
# Coverage report will be in coverage/index.html
|
|
247
|
+
open coverage/index.html
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
## Development
|
|
252
|
+
|
|
253
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests (note: currently, the gem has a basic structure and tests will be expanded with functionality). You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
254
|
+
|
|
255
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
256
|
+
|
|
257
|
+
## Contributing
|
|
258
|
+
|
|
259
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/durable-oss/typst-rails](https://github.com/durable-oss/typst-rails). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://www.contributor-covenant.org) code of conduct.
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
264
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypstRails
|
|
4
|
+
module Backends
|
|
5
|
+
# Abstract interface for a Typst compilation backend.
|
|
6
|
+
#
|
|
7
|
+
# A backend is responsible for turning a Typst source file (plus a root
|
|
8
|
+
# directory for relative imports) into PDF bytes. Subclasses implement
|
|
9
|
+
# {#available?} and {#compile}.
|
|
10
|
+
#
|
|
11
|
+
# @abstract
|
|
12
|
+
class Base
|
|
13
|
+
# @return [Boolean] whether this backend can be used in the current environment
|
|
14
|
+
# (e.g. the `typst` executable is on PATH, or the `typst` gem is loaded)
|
|
15
|
+
def available?
|
|
16
|
+
raise NotImplementedError, "#{self.class} must implement #available?"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Compiles a Typst source file to PDF.
|
|
20
|
+
#
|
|
21
|
+
# @param typ_path [String] path to the .typ source file to compile
|
|
22
|
+
# @param root_dir [String] root directory for resolving relative imports
|
|
23
|
+
# @return [String] binary PDF data
|
|
24
|
+
# @raise [TypstRails::Error] if compilation fails
|
|
25
|
+
def compile(typ_path, root_dir)
|
|
26
|
+
raise NotImplementedError, "#{self.class} must implement #compile"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "typst_rails/backends/base"
|
|
5
|
+
|
|
6
|
+
module TypstRails
|
|
7
|
+
module Backends
|
|
8
|
+
# Compiles Typst documents by shelling out to the `typst` CLI executable.
|
|
9
|
+
#
|
|
10
|
+
# This is the original TypstRails backend and remains the default when the
|
|
11
|
+
# `typst` gem is not installed. It requires Typst to be installed separately
|
|
12
|
+
# and available on PATH (or at {TypstRails::Configuration#typst_executable_path}).
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# backend = TypstRails::Backends::Cli.new
|
|
16
|
+
# backend.available? #=> true, if `typst` is on PATH
|
|
17
|
+
# pdf_data = backend.compile("/tmp/doc.typ", "/tmp")
|
|
18
|
+
class Cli < Base
|
|
19
|
+
# @param executable_path [String, nil] path to (or name of) the Typst executable.
|
|
20
|
+
# Defaults to {TypstRails::Configuration#typst_executable_path}, falling back to "typst".
|
|
21
|
+
def initialize(executable_path: nil)
|
|
22
|
+
super()
|
|
23
|
+
@executable_path = executable_path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Boolean] whether the configured Typst executable can be found
|
|
27
|
+
def available?
|
|
28
|
+
return true if File.exist?(executable_path)
|
|
29
|
+
|
|
30
|
+
system("which #{executable_path} > /dev/null 2>&1") == true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @see Base#compile
|
|
34
|
+
def compile(typ_path, root_dir)
|
|
35
|
+
output_pdf_path = File.join(root_dir, "#{File.basename(typ_path, ".typ")}_output.pdf")
|
|
36
|
+
cmd = [executable_path, "compile", "--root", root_dir, typ_path, output_pdf_path]
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
_stdout_str, stderr_str, status = Open3.capture3(*cmd)
|
|
40
|
+
rescue Errno::ENOENT => e
|
|
41
|
+
raise Error, "Typst executable not found. Please ensure Typst is installed and in your PATH. (#{e.message})"
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
raise Error, "Failed to execute Typst compiler: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
raise_compilation_failure(cmd, stderr_str) unless status.success?
|
|
47
|
+
read_compiled_pdf(output_pdf_path)
|
|
48
|
+
ensure
|
|
49
|
+
safe_unlink(output_pdf_path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# The safe-navigation operator does not help here: TypstRails.configuration
|
|
55
|
+
# raises NoMethodError when the top-level TypstRails module has not been
|
|
56
|
+
# loaded, which happens when "typst_rails/renderer" is required on its own.
|
|
57
|
+
def executable_path
|
|
58
|
+
@executable_path || configured_executable_path || "typst"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def configured_executable_path
|
|
62
|
+
return nil unless TypstRails.respond_to?(:configuration)
|
|
63
|
+
|
|
64
|
+
TypstRails.configuration&.typst_executable_path
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def read_compiled_pdf(output_pdf_path)
|
|
68
|
+
raise Error, "Typst compilation succeeded but output file was not created" unless File.exist?(output_pdf_path)
|
|
69
|
+
|
|
70
|
+
pdf_data = File.binread(output_pdf_path)
|
|
71
|
+
raise Error, "Typst compilation produced an empty PDF" if pdf_data.empty?
|
|
72
|
+
|
|
73
|
+
pdf_data
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def raise_compilation_failure(cmd, stderr_str)
|
|
77
|
+
error_message = "Typst compilation failed.\n"
|
|
78
|
+
error_message += "Command: #{cmd.join(" ")}\n"
|
|
79
|
+
error_message += "Stderr: #{stderr_str}" unless stderr_str.empty?
|
|
80
|
+
raise Error, error_message
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def safe_unlink(path)
|
|
84
|
+
File.unlink(path) if path && File.exist?(path)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
warn "TypstRails: Failed to clean up temp PDF file: #{e.message}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "typst_rails/backends/base"
|
|
4
|
+
|
|
5
|
+
module TypstRails
|
|
6
|
+
module Backends
|
|
7
|
+
# Compiles Typst documents in-process using the `typst` RubyGem
|
|
8
|
+
# (https://rubygems.org/gems/typst), a native extension that embeds the
|
|
9
|
+
# Typst compiler.
|
|
10
|
+
#
|
|
11
|
+
# This avoids the cost of spawning a subprocess for every render and does
|
|
12
|
+
# not require a separately installed `typst` executable. It is used
|
|
13
|
+
# automatically when the `typst` gem is available; add it to your Gemfile
|
|
14
|
+
# to opt in:
|
|
15
|
+
#
|
|
16
|
+
# gem "typst"
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# backend = TypstRails::Backends::Gem.new
|
|
20
|
+
# backend.available? #=> true, if the `typst` gem is loaded
|
|
21
|
+
# pdf_data = backend.compile("/tmp/doc.typ", "/tmp")
|
|
22
|
+
class Gem < Base
|
|
23
|
+
# @return [Boolean] whether the `typst` gem is installed and loadable
|
|
24
|
+
def available?
|
|
25
|
+
return true if defined?(::Typst::Pdf)
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
require "typst"
|
|
29
|
+
true
|
|
30
|
+
rescue LoadError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @see Base#compile
|
|
36
|
+
def compile(typ_path, root_dir)
|
|
37
|
+
::Typst::Pdf.new(file: typ_path, root: root_dir).compiled.document
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
raise Error, "Typst compilation failed: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "typst_rails/backends/base"
|
|
4
|
+
|
|
5
|
+
module TypstRails
|
|
6
|
+
module Backends
|
|
7
|
+
# Registry of available Typst compilation backends, and resolver that
|
|
8
|
+
# picks the right one to use for a given configuration.
|
|
9
|
+
#
|
|
10
|
+
# Backends are tried in registration order; the first one whose
|
|
11
|
+
# {Base#available?} returns true wins. This lets TypstRails prefer the
|
|
12
|
+
# `typst` gem (fast, in-process) when it's installed, and fall back to
|
|
13
|
+
# shelling out to the `typst` CLI otherwise.
|
|
14
|
+
#
|
|
15
|
+
# @example Registering a custom backend
|
|
16
|
+
# TypstRails::Backends::Registry.register(:my_backend, MyBackend.new)
|
|
17
|
+
# TypstRails.configure { |c| c.backend = :my_backend }
|
|
18
|
+
module Registry
|
|
19
|
+
class << self
|
|
20
|
+
# @return [Hash{Symbol => Base}]
|
|
21
|
+
def backends
|
|
22
|
+
@backends ||= {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Array<Symbol>] backend names in resolution priority order
|
|
26
|
+
def priority
|
|
27
|
+
@priority ||= []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Registers a backend under the given name.
|
|
31
|
+
#
|
|
32
|
+
# @param name [Symbol] the backend's identifier (e.g. :gem, :cli)
|
|
33
|
+
# @param backend [Base] the backend instance
|
|
34
|
+
# @param priority [Boolean] whether to append this name to the auto-detection order
|
|
35
|
+
# @return [void]
|
|
36
|
+
def register(name, backend, priority: true)
|
|
37
|
+
backends[name.to_sym] = backend
|
|
38
|
+
self.priority << name.to_sym if priority && !self.priority.include?(name.to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Resolves which backend to use.
|
|
42
|
+
#
|
|
43
|
+
# @param preference [Symbol, Base, nil] an explicit backend name, a backend
|
|
44
|
+
# instance, or nil/:auto to pick the first available backend in priority order
|
|
45
|
+
# @return [Base] the resolved backend
|
|
46
|
+
# @raise [TypstRails::Error] if no backend is available
|
|
47
|
+
def resolve(preference = :auto)
|
|
48
|
+
return preference if preference.is_a?(Base)
|
|
49
|
+
return fetch_named_backend(preference) if preference && preference != :auto
|
|
50
|
+
|
|
51
|
+
auto_detect
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Clears all registered backends. Primarily useful for tests.
|
|
55
|
+
# @return [void]
|
|
56
|
+
def reset!
|
|
57
|
+
@backends = {}
|
|
58
|
+
@priority = []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def fetch_named_backend(name)
|
|
64
|
+
backend = backends[name.to_sym]
|
|
65
|
+
raise Error, "Unknown Typst backend: #{name.inspect}" unless backend
|
|
66
|
+
|
|
67
|
+
backend
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def auto_detect
|
|
71
|
+
priority.each do |name|
|
|
72
|
+
backend = backends[name]
|
|
73
|
+
return backend if backend&.available?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
raise Error, "No Typst backend is available. Install the `typst` gem, or " \
|
|
77
|
+
"install the Typst CLI and ensure it is on PATH."
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "typst_rails/backends/base"
|
|
4
|
+
require "typst_rails/backends/registry"
|
|
5
|
+
require "typst_rails/backends/cli"
|
|
6
|
+
require "typst_rails/backends/gem"
|
|
7
|
+
|
|
8
|
+
module TypstRails
|
|
9
|
+
module Backends
|
|
10
|
+
# The `typst` gem, when installed, compiles in-process and is preferred
|
|
11
|
+
# over shelling out. The CLI backend is always registered as a fallback.
|
|
12
|
+
Registry.register(:gem, Gem.new)
|
|
13
|
+
Registry.register(:cli, Cli.new)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypstRails
|
|
4
|
+
# Detects which web framework (if any) is currently loaded
|
|
5
|
+
module FrameworkDetection
|
|
6
|
+
def self.rails?
|
|
7
|
+
defined?(::Rails) && ::Rails.respond_to?(:application)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.rage?
|
|
11
|
+
defined?(::Rage::Application)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.sinatra?
|
|
15
|
+
defined?(::Sinatra::Base)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.detected_framework
|
|
19
|
+
return :rails if rails?
|
|
20
|
+
return :rage if rage?
|
|
21
|
+
return :sinatra if sinatra?
|
|
22
|
+
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|