typstify 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 +49 -0
- data/LICENSE +21 -0
- data/README.md +235 -0
- data/SECURITY.md +47 -0
- data/fonts/inter/Inter-Regular.ttf +0 -0
- data/fonts/inter/Inter-SemiBold.ttf +0 -0
- data/fonts/inter/OFL.txt +92 -0
- data/lib/generators/typstify/install/install_generator.rb +39 -0
- data/lib/generators/typstify/install/templates/typstify.rb +43 -0
- data/lib/generators/typstify/template/template_generator.rb +67 -0
- data/lib/tasks/typstify.rake +43 -0
- data/lib/typstify/adapter.rb +56 -0
- data/lib/typstify/config.rb +106 -0
- data/lib/typstify/data.rb +103 -0
- data/lib/typstify/document.rb +63 -0
- data/lib/typstify/engine.rb +38 -0
- data/lib/typstify/erb_pipeline.rb +44 -0
- data/lib/typstify/errors.rb +68 -0
- data/lib/typstify/escaping.rb +45 -0
- data/lib/typstify/fonts.rb +175 -0
- data/lib/typstify/renderer.rb +43 -0
- data/lib/typstify/resolver.rb +62 -0
- data/lib/typstify/version.rb +5 -0
- data/lib/typstify/warnings.rb +65 -0
- data/lib/typstify/workspace.rb +85 -0
- data/lib/typstify.rb +104 -0
- data/templates/certificate/certificate.typ +70 -0
- data/templates/certificate/sample_data.json +13 -0
- data/templates/invoice/invoice.typ +57 -0
- data/templates/invoice/sample_data.json +21 -0
- data/templates/receipt/receipt.typ +69 -0
- data/templates/receipt/sample_data.json +17 -0
- data/templates/report/report.typ +103 -0
- data/templates/report/sample_data.json +49 -0
- data/templates/shared/branding.typ +140 -0
- metadata +164 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 02ec11e818e2f0b65a784efb1db7e5e6aeb45f95f3e06189a8bf1ebc56d24811
|
|
4
|
+
data.tar.gz: ff48f408265ec390b57cc9f071edda050747b11795e15d2a6f07a34ff2037ddf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 37e3c8c203c967dbfbec72dc0023dae2428919a3606938b00a88ae29b0fd6034cace8481d1db610d51496d0f78e83cf75c2eedcc0f6f18484bbc6eaebd88f6ca
|
|
7
|
+
data.tar.gz: 29960b89f82e52cd8878a6fdac2a8a978c9acbefa3e11f8e69a96e3fb6d94dadfd52c795d513fe13e1c46a1ad2007ef3985c1047643ace49ed314db70d0581f1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] — 2026-08-31
|
|
10
|
+
|
|
11
|
+
First release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `render pdf:` renderer for any Rails controller, honouring `data:`, `filename:`,
|
|
16
|
+
`disposition:` and `status:`.
|
|
17
|
+
- `Typstify.render` and `Typstify.render_and_attach` for jobs, mailers and ActiveStorage.
|
|
18
|
+
- **Data mode**: templates are pure `.typ` and read their input from `data.json`, so user
|
|
19
|
+
content is data rather than source and cannot alter the document.
|
|
20
|
+
- **Workspace isolation**: every render compiles in a fresh temporary directory containing only
|
|
21
|
+
the template, its `shared/` partials and `data.json`, with Typst rooted there. Symlinks in the
|
|
22
|
+
shared tree are skipped rather than followed out of the root.
|
|
23
|
+
- **ERB mode** (`.typ.erb`) with the `typ()` escaping helper, one spec per escaped character.
|
|
24
|
+
- Starter template pack: invoice, receipt, report and certificate, each with sample data and a
|
|
25
|
+
shared `branding.typ`.
|
|
26
|
+
- `rails g typstify:install` and `rails g typstify:template <name>`.
|
|
27
|
+
- `rake typstify:preview[invoices/show]` and `rake typstify:templates`.
|
|
28
|
+
- PDF standard selection: `:ua_1` (PDF/UA-1), `:a_1b`, `:a_2b`, `:a_3b`, `:a_4`, `:pdf_1_7`,
|
|
29
|
+
`:pdf_2_0`.
|
|
30
|
+
- Strict font checking: a family a template names but nothing can supply raises
|
|
31
|
+
`FontMissingError` in development and test, and warns in production.
|
|
32
|
+
- `ignore_system_fonts`, which skips the operating system's font directories. Worth knowing:
|
|
33
|
+
that scan costs roughly 50 ms per render — the starter invoice goes from 58 ms to 4 ms.
|
|
34
|
+
- `package_cache` for vendored Typst Universe packages, for builds with no network.
|
|
35
|
+
- Inter (SIL OFL) bundled, so the starter templates render with no configuration.
|
|
36
|
+
|
|
37
|
+
### Known limitations
|
|
38
|
+
|
|
39
|
+
- `on_warning` does not fire for warnings from a **successful** compile. The `typst` binding
|
|
40
|
+
discards them; it only formats warnings into the message when compilation fails. The hook
|
|
41
|
+
works for failure-path warnings and for this gem's own font check. Fix opened upstream:
|
|
42
|
+
[actsasflinn/typst-rb#10](https://github.com/actsasflinn/typst-rb/pull/10).
|
|
43
|
+
- `package_cache` is effective on Linux, where the platform data directory follows
|
|
44
|
+
`XDG_DATA_HOME`. macOS uses a fixed location that no environment variable redirects.
|
|
45
|
+
- Ruby threads do not speed up rendering: the binding holds the GVL through compilation. Use
|
|
46
|
+
processes for throughput.
|
|
47
|
+
|
|
48
|
+
[Unreleased]: https://github.com/TheSoloHacker47/typstify/compare/v0.1.0...HEAD
|
|
49
|
+
[0.1.0]: https://github.com/TheSoloHacker47/typstify/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nikhil Nelson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# typstify
|
|
2
|
+
|
|
3
|
+
> PDF generation for Rails on the [Typst](https://typst.app) engine — the wkhtmltopdf replacement.
|
|
4
|
+
> Invoices, reports, certificates: fast, beautiful, accessible (PDF/UA), and no headless Chrome in your Docker image.
|
|
5
|
+
|
|
6
|
+
[](https://rubygems.org/gems/typstify)
|
|
7
|
+
[](https://github.com/TheSoloHacker47/typstify/actions)
|
|
8
|
+
[](https://github.com/TheSoloHacker47/typstify/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Your Rails app generates PDFs. Your options until now:
|
|
13
|
+
|
|
14
|
+
| | problem |
|
|
15
|
+
|---|---|
|
|
16
|
+
| wicked_pdf / pdfkit | built on **wkhtmltopdf, which is archived and unmaintained** — no security fixes, ancient WebKit |
|
|
17
|
+
| grover / Chrome-based | ships a **full browser** in your production image: hundreds of MB, memory spikes, cold starts |
|
|
18
|
+
| prawn | hand-positioning a coordinate DSL for every invoice line |
|
|
19
|
+
|
|
20
|
+
[Typst](https://typst.app) is the Rust-based successor to LaTeX: a markup language that
|
|
21
|
+
compiles to PDF in **milliseconds**, reads **JSON natively**, and can emit **PDF/UA-1
|
|
22
|
+
accessible PDFs** — which the European Accessibility Act now effectively requires for
|
|
23
|
+
machine-generated customer documents. `typstify` makes it feel like Rails.
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Gemfile
|
|
29
|
+
gem "typstify"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle && rails g typstify:install && rails g typstify:template invoice
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# app/controllers/invoices_controller.rb
|
|
38
|
+
def show
|
|
39
|
+
invoice = Invoice.find(params[:id])
|
|
40
|
+
render pdf: "invoices/show",
|
|
41
|
+
data: { number: invoice.number,
|
|
42
|
+
total: invoice.total.format,
|
|
43
|
+
line_items: invoice.line_items.map { |li| { name: li.name, qty: li.qty, amount: li.amount.format } } },
|
|
44
|
+
filename: "invoice-#{invoice.number}.pdf"
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```typ
|
|
49
|
+
// app/views/invoices/show.typ
|
|
50
|
+
#let data = json("data.json")
|
|
51
|
+
|
|
52
|
+
= Invoice #data.number
|
|
53
|
+
|
|
54
|
+
#table(
|
|
55
|
+
columns: (1fr, auto, auto),
|
|
56
|
+
[*Item*], [*Qty*], [*Amount*],
|
|
57
|
+
..data.line_items.map(i => (i.name, str(i.qty), i.amount)).flatten()
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
#align(right)[*Total: #data.total*]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
That's a complete, styled, paginated invoice. No browser. No dead binaries.
|
|
64
|
+
|
|
65
|
+
## The data-first design (and why it's safer)
|
|
66
|
+
|
|
67
|
+
Notice what the template does: it reads your data as **JSON**, it doesn't interpolate strings.
|
|
68
|
+
Your Ruby data can contain anything — Typst markup, `#` directives, quotes — and it renders as
|
|
69
|
+
literal text, because content never becomes code.
|
|
70
|
+
|
|
71
|
+
Each render also happens in a fresh temporary directory containing only the template, its
|
|
72
|
+
`shared/` partials, and `data.json`. Typst is invoked with that directory as its compile root,
|
|
73
|
+
so a template physically cannot read your `.env`, your credentials, or any other view. Not
|
|
74
|
+
because we filter the path — because the file is not there.
|
|
75
|
+
|
|
76
|
+
`spec/security/` asserts both properties directly; they are the specs worth reading first.
|
|
77
|
+
|
|
78
|
+
Prefer classic ERB? `.typ.erb` templates work too — pass every dynamic value through the
|
|
79
|
+
`typ()` escaping helper:
|
|
80
|
+
|
|
81
|
+
```erb
|
|
82
|
+
= Hello <%= typ(@user.display_name) %>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
But use data mode. Future-you says thanks.
|
|
86
|
+
|
|
87
|
+
## Everywhere you need a PDF
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# Background jobs / plain Ruby
|
|
91
|
+
pdf_bytes = Typstify.render(template: "reports/monthly", data: payload)
|
|
92
|
+
|
|
93
|
+
# Mailers
|
|
94
|
+
attachments["report.pdf"] = Typstify.render(template: "reports/monthly", data: payload)
|
|
95
|
+
|
|
96
|
+
# ActiveStorage
|
|
97
|
+
Typstify.render_and_attach(user.documents, template: "certificates/completion",
|
|
98
|
+
data: cert_data, filename: "certificate.pdf")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Batch-generating 10,000 invoices in a job? Typst's per-document compile time is milliseconds —
|
|
102
|
+
see [`docs/batch-generation.md`](https://github.com/TheSoloHacker47/typstify/blob/main/docs/batch-generation.md) for a Sidekiq/Solid Queue pattern
|
|
103
|
+
and the two settings that make output reproducible.
|
|
104
|
+
|
|
105
|
+
## Starter templates
|
|
106
|
+
|
|
107
|
+
`rails g typstify:template <name>` for: **invoice** · **receipt** · **report** ·
|
|
108
|
+
**certificate**. Each comes with sample data and a preview task:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
rake typstify:preview[invoices/show]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
which compiles with the bundled `sample_data.json` into `tmp/previews/`, so you can iterate on
|
|
115
|
+
design without loading your app's initializers or touching the database. Shared branding (logo,
|
|
116
|
+
colours, footer) lives in `app/views/shared/branding.typ` — change it once, every document
|
|
117
|
+
updates.
|
|
118
|
+
|
|
119
|
+
## Accessible PDFs (EAA / ADA)
|
|
120
|
+
|
|
121
|
+
HTML→PDF pipelines produce untagged PDFs that fail accessibility requirements. Typst supports
|
|
122
|
+
PDF/UA-1 output; enable it globally:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
Typstify.configure { |c| c.pdf_standard = :ua_1 }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Also available: `:a_1b`, `:a_2b`, `:a_3b`, `:a_4`, `:pdf_1_7`, `:pdf_2_0`. PDF/UA-1 requires a
|
|
129
|
+
document title, so set `#set document(title: "…")` in the template.
|
|
130
|
+
|
|
131
|
+
If you invoice EU customers, your compliance team will hug you.
|
|
132
|
+
|
|
133
|
+
## Fonts & Docker (read this before deploying)
|
|
134
|
+
|
|
135
|
+
Typst uses system fonts plus any directories you configure. The reliable production setup is
|
|
136
|
+
vendoring fonts in your app:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
c.font_paths = [Rails.root.join("app/assets/fonts")]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A missing font family fails loudly in development and test (`strict_fonts`, on by default
|
|
143
|
+
there) instead of silently substituting a different face in production. Inter ships with the
|
|
144
|
+
gem, so the starter templates work with no configuration at all.
|
|
145
|
+
|
|
146
|
+
Once your fonts are vendored, turn off the system font scan. It costs about **50 ms per
|
|
147
|
+
render** — the starter invoice goes from 58 ms to 4 ms:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
c.ignore_system_fonts = true
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Typst Universe packages can be vendored into a directory for network-free builds — with a
|
|
154
|
+
platform caveat that matters. Full guide: [`docs/fonts-and-docker.md`](https://github.com/TheSoloHacker47/typstify/blob/main/docs/fonts-and-docker.md).
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# config/initializers/typstify.rb — written by the install generator
|
|
160
|
+
Typstify.configure do |c|
|
|
161
|
+
c.template_root = Rails.root.join("app/views") # default
|
|
162
|
+
c.shared_dir = "shared" # partials namespace
|
|
163
|
+
c.font_paths = [Rails.root.join("app/assets/fonts")]
|
|
164
|
+
c.ignore_system_fonts = true # false by default; see below
|
|
165
|
+
c.package_cache = Rails.root.join("vendor/typst_packages") # vendored Universe packages
|
|
166
|
+
c.pdf_standard = :ua_1 # nil by default
|
|
167
|
+
c.strict_fonts = Rails.env.local? # the default
|
|
168
|
+
c.on_warning = ->(warnings, template) { Rails.logger.warn("#{template}: #{warnings}") }
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### One honest caveat about `on_warning`
|
|
173
|
+
|
|
174
|
+
The `typst` binding this gem depends on discards the compiler's warnings when compilation
|
|
175
|
+
**succeeds** — it only formats them into the message when compilation fails. So `on_warning`
|
|
176
|
+
fires for warnings that accompany a compile error, and for the font check this gem performs
|
|
177
|
+
itself, but not yet for arbitrary success-path warnings. There is nothing to listen to until
|
|
178
|
+
that changes upstream, so the fix is in flight there:
|
|
179
|
+
[actsasflinn/typst-rb#10](https://github.com/actsasflinn/typst-rb/pull/10). Rather than ship an
|
|
180
|
+
option that quietly never fires, the missing-font case — the one that actually bites people —
|
|
181
|
+
is checked here, before the compiler runs.
|
|
182
|
+
|
|
183
|
+
## Errors
|
|
184
|
+
|
|
185
|
+
| | when |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `Typstify::MissingTemplate` | neither `.typ` nor `.typ.erb` exists; the message lists both paths tried |
|
|
188
|
+
| `Typstify::CompileError` | Typst rejected the document; carries its annotated diagnostic and the line, with your template's name substituted for the workspace path |
|
|
189
|
+
| `Typstify::FontMissingError` | a template names a font nothing can supply, in strict mode |
|
|
190
|
+
| `Typstify::PathError` | a template name resolved outside `template_root` |
|
|
191
|
+
| `ArgumentError` | `data:` contains something JSON cannot represent; the message names the key path, e.g. `data.line_items[0].amount` |
|
|
192
|
+
|
|
193
|
+
## Migrating from wicked_pdf
|
|
194
|
+
|
|
195
|
+
There's no HTML→Typst converter — you rewrite each PDF view, and that's genuinely fine: most
|
|
196
|
+
teams have 2–5 PDF views, and each takes minutes, not hours, in Typst. The side-by-side
|
|
197
|
+
walkthrough (a real wicked_pdf invoice vs its Typst version, at a third of the line count):
|
|
198
|
+
[`docs/migrating-from-wicked_pdf.md`](https://github.com/TheSoloHacker47/typstify/blob/main/docs/migrating-from-wicked_pdf.md).
|
|
199
|
+
|
|
200
|
+
## Compatibility
|
|
201
|
+
|
|
202
|
+
Ruby ≥ 3.1 · Rails 7.1 – 8.1 · Linux & macOS. PDF compilation is done by the excellent
|
|
203
|
+
[`typst` gem](https://github.com/actsasflinn/typst-rb) (Rust binding to the Typst compiler) —
|
|
204
|
+
this gem is the Rails layer on top, and ships no compiler of its own.
|
|
205
|
+
|
|
206
|
+
Note: Rails 8.1 requires Ruby 3.4 or newer in practice, because `actionview` uses syntax that
|
|
207
|
+
older Rubies cannot parse. The CI matrix pairs them accordingly.
|
|
208
|
+
|
|
209
|
+
## Related work
|
|
210
|
+
|
|
211
|
+
[`typst-rails`](https://github.com/durable-oss/typst-rails) by David J. Berube covers similar
|
|
212
|
+
ground and appeared around the same time; it supports Rage and Sinatra alongside Rails. This
|
|
213
|
+
gem is narrower on purpose — Rails only — and spends that focus on the sandboxed workspace, the
|
|
214
|
+
data-mode security model, the starter template pack and PDF/UA output. Worth comparing both
|
|
215
|
+
against what you need.
|
|
216
|
+
|
|
217
|
+
## Contributing
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
git clone https://github.com/TheSoloHacker47/typstify && cd typstify
|
|
221
|
+
bin/setup && bundle exec rake
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Template pack contributions especially welcome — new document types need sample data, a spec,
|
|
225
|
+
and a golden. See [CONTRIBUTING.md](https://github.com/TheSoloHacker47/typstify/blob/main/CONTRIBUTING.md).
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
[MIT](https://github.com/TheSoloHacker47/typstify/blob/main/LICENSE). Inter is vendored under the [SIL Open Font License](https://github.com/TheSoloHacker47/typstify/blob/main/fonts/inter/OFL.txt).
|
|
230
|
+
Typst itself is Apache-2.0 by the Typst team — go star it.
|
|
231
|
+
|
|
232
|
+
## More from me
|
|
233
|
+
|
|
234
|
+
- [scrubber_rb](https://github.com/TheSoloHacker47/scrubber-rb) — fast PII and secret redaction for Ruby, with a Rust core
|
|
235
|
+
- [bundler-overrule](https://github.com/TheSoloHacker47/bundler-overrule) — force, ban and swap gem versions in your Gemfile
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
The latest minor release receives security fixes.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Report privately through GitHub's
|
|
10
|
+
[security advisory form](https://github.com/TheSoloHacker47/typstify/security/advisories/new),
|
|
11
|
+
or by email to thesolohacker47@gmail.com. Please do not open a public issue first.
|
|
12
|
+
|
|
13
|
+
Expect an acknowledgement within 72 hours, and a fix or a plan within 14 days for anything
|
|
14
|
+
confirmed.
|
|
15
|
+
|
|
16
|
+
## What counts as a vulnerability here
|
|
17
|
+
|
|
18
|
+
This gem makes two claims. A credible way to break either is a vulnerability:
|
|
19
|
+
|
|
20
|
+
1. **Data cannot become code.** Anything passed as `data:` reaches the template through
|
|
21
|
+
`data.json` and is read as a string. If you can make a value passed through `data:` execute
|
|
22
|
+
as Typst — call a function, read a file, alter the document's structure — that is a
|
|
23
|
+
vulnerability.
|
|
24
|
+
2. **A template cannot read outside its workspace.** Each render compiles in a temporary
|
|
25
|
+
directory holding only the template, its `shared/` partials and `data.json`, with Typst
|
|
26
|
+
rooted there. If you can make a template read a file that was not copied in for it — through
|
|
27
|
+
a symlink, a path in `template_root`, a package import, or anything else — that is a
|
|
28
|
+
vulnerability.
|
|
29
|
+
|
|
30
|
+
Also in scope: a way to make `Typstify::Resolver` open a file outside `template_root`, and a
|
|
31
|
+
way to make `Typstify::Escaping.typ` emit a string that Typst still parses as markup.
|
|
32
|
+
|
|
33
|
+
## What does not count
|
|
34
|
+
|
|
35
|
+
- **ERB mode without `typ()`.** `.typ.erb` interpolates Ruby strings into Typst source; an
|
|
36
|
+
unescaped value there is injection by construction, which is why `typ()` exists and why the
|
|
37
|
+
README says to use data mode instead. `spec/security/erb_mode_spec.rb` documents this
|
|
38
|
+
explicitly.
|
|
39
|
+
- **Templates authored by an attacker.** A `.typ` file in `app/views` is code you deployed. The
|
|
40
|
+
workspace boundary limits what it can reach, but the threat model is untrusted *data*, not
|
|
41
|
+
untrusted templates.
|
|
42
|
+
- **Vulnerabilities in the Typst compiler itself.** Report those to
|
|
43
|
+
[typst/typst](https://github.com/typst/typst); we will pick up the fix when the binding does.
|
|
44
|
+
|
|
45
|
+
## Credit
|
|
46
|
+
|
|
47
|
+
Reporters are credited in the changelog and the advisory unless they ask not to be.
|
|
Binary file
|
|
Binary file
|
data/fonts/inter/OFL.txt
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
-----------------------------------------------------------
|
|
8
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
PREAMBLE
|
|
12
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
13
|
+
development of collaborative font projects, to support the font creation
|
|
14
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
15
|
+
open framework in which fonts may be shared and improved in partnership
|
|
16
|
+
with others.
|
|
17
|
+
|
|
18
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
19
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
redistributed and/or sold with any software provided that any reserved
|
|
22
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
23
|
+
however, cannot be released under any other type of license. The
|
|
24
|
+
requirement for fonts to remain under this license does not apply
|
|
25
|
+
to any document created using the fonts or their derivatives.
|
|
26
|
+
|
|
27
|
+
DEFINITIONS
|
|
28
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
29
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
30
|
+
include source files, build scripts and documentation.
|
|
31
|
+
|
|
32
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
33
|
+
copyright statement(s).
|
|
34
|
+
|
|
35
|
+
"Original Version" refers to the collection of Font Software components as
|
|
36
|
+
distributed by the Copyright Holder(s).
|
|
37
|
+
|
|
38
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
39
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
40
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
41
|
+
new environment.
|
|
42
|
+
|
|
43
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
44
|
+
writer or other person who contributed to the Font Software.
|
|
45
|
+
|
|
46
|
+
PERMISSION AND CONDITIONS
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
49
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
50
|
+
Software, subject to the following conditions:
|
|
51
|
+
|
|
52
|
+
1) Neither the Font Software nor any of its individual components,
|
|
53
|
+
in Original or Modified Versions, may be sold by itself.
|
|
54
|
+
|
|
55
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
56
|
+
redistributed and/or sold with any software, provided that each copy
|
|
57
|
+
contains the above copyright notice and this license. These can be
|
|
58
|
+
included either as stand-alone text files, human-readable headers or
|
|
59
|
+
in the appropriate machine-readable metadata fields within text or
|
|
60
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
61
|
+
|
|
62
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
63
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
64
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
65
|
+
presented to the users.
|
|
66
|
+
|
|
67
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
68
|
+
Software shall not be used to promote, endorse or advertise any
|
|
69
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
70
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
71
|
+
permission.
|
|
72
|
+
|
|
73
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
74
|
+
must be distributed entirely under this license, and must not be
|
|
75
|
+
distributed under any other license. The requirement for fonts to
|
|
76
|
+
remain under this license does not apply to any document created
|
|
77
|
+
using the Font Software.
|
|
78
|
+
|
|
79
|
+
TERMINATION
|
|
80
|
+
This license becomes null and void if any of the above conditions are
|
|
81
|
+
not met.
|
|
82
|
+
|
|
83
|
+
DISCLAIMER
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
88
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Typstify
|
|
6
|
+
module Generators
|
|
7
|
+
# rails g typstify:install
|
|
8
|
+
#
|
|
9
|
+
# Drops in the initializer and the shared branding partial every starter
|
|
10
|
+
# template imports.
|
|
11
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Creates config/initializers/typstify.rb and app/views/shared/branding.typ"
|
|
15
|
+
|
|
16
|
+
def create_initializer
|
|
17
|
+
template "typstify.rb", "config/initializers/typstify.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_branding
|
|
21
|
+
copy_file Typstify.template_pack_path.join("shared", "branding.typ").to_s,
|
|
22
|
+
"app/views/shared/branding.typ"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def report
|
|
26
|
+
say ""
|
|
27
|
+
say "Typstify installed.", :green
|
|
28
|
+
say ""
|
|
29
|
+
say " Next: rails g typstify:template invoice"
|
|
30
|
+
say " rake typstify:preview[invoices/show]"
|
|
31
|
+
say ""
|
|
32
|
+
say " Fonts: templates use Inter, which ships with the gem. To use your own,"
|
|
33
|
+
say " vendor the files and set c.font_paths in the initializer."
|
|
34
|
+
say " See https://github.com/TheSoloHacker47/typstify/blob/main/docs/fonts-and-docker.md"
|
|
35
|
+
say ""
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Typstify — PDF generation on the Typst engine.
|
|
4
|
+
# https://github.com/TheSoloHacker47/typstify
|
|
5
|
+
|
|
6
|
+
Typstify.configure do |c|
|
|
7
|
+
# Where .typ templates live. They are views like any other.
|
|
8
|
+
c.template_root = Rails.root.join("app/views")
|
|
9
|
+
|
|
10
|
+
# Subdirectory of template_root copied into every compile, so
|
|
11
|
+
# `#import "shared/branding.typ"` resolves from any template.
|
|
12
|
+
c.shared_dir = "shared"
|
|
13
|
+
|
|
14
|
+
# Extra font directories. The gem's bundled Inter is always on the path.
|
|
15
|
+
# Vendoring the fonts your templates name is the reliable production setup —
|
|
16
|
+
# see docs/fonts-and-docker.md.
|
|
17
|
+
# c.font_paths = [Rails.root.join("app/assets/fonts")]
|
|
18
|
+
|
|
19
|
+
# Skip the operating system's font directories. Once your fonts are vendored
|
|
20
|
+
# above, nothing on the system is being used and scanning for it costs about
|
|
21
|
+
# 50 ms per render: the starter invoice goes from 58 ms to 4 ms. Off by
|
|
22
|
+
# default because it changes which face a template naming "Helvetica" gets.
|
|
23
|
+
# c.ignore_system_fonts = true
|
|
24
|
+
|
|
25
|
+
# Vendored Typst Universe packages, for builds with no network.
|
|
26
|
+
# Effective on Linux (so: in your Docker image); see the same doc for why.
|
|
27
|
+
# c.package_cache = Rails.root.join("vendor/typst_packages")
|
|
28
|
+
|
|
29
|
+
# Accessible output. :ua_1 is PDF/UA-1, which the European Accessibility Act
|
|
30
|
+
# effectively requires of machine-generated customer documents.
|
|
31
|
+
# Also available: :a_1b, :a_2b, :a_3b, :a_4, :pdf_1_7, :pdf_2_0.
|
|
32
|
+
# c.pdf_standard = :ua_1
|
|
33
|
+
|
|
34
|
+
# Raise instead of warning when a template names a font nothing can supply.
|
|
35
|
+
# Defaults to true in development and test, false in production.
|
|
36
|
+
# c.strict_fonts = Rails.env.local?
|
|
37
|
+
|
|
38
|
+
# Where warnings go. See the README for what this currently does and does not
|
|
39
|
+
# catch — the compiler binding drops warnings on a successful compile.
|
|
40
|
+
# c.on_warning = lambda do |warnings, template|
|
|
41
|
+
# Rails.logger.warn("[typstify] #{template}: #{warnings.join("; ")}")
|
|
42
|
+
# end
|
|
43
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Typstify
|
|
6
|
+
module Generators
|
|
7
|
+
# rails g typstify:template invoice
|
|
8
|
+
#
|
|
9
|
+
# Copies a starter template and its sample data into app/views, so the
|
|
10
|
+
# preview task works immediately and you have something real to edit.
|
|
11
|
+
class TemplateGenerator < ::Rails::Generators::NamedBase
|
|
12
|
+
AVAILABLE = %w[invoice receipt report certificate].freeze
|
|
13
|
+
|
|
14
|
+
# The gem's own template pack is the source: this generator copies
|
|
15
|
+
# starter documents, it does not render ERB of its own.
|
|
16
|
+
source_root Typstify.template_pack_path.to_s
|
|
17
|
+
|
|
18
|
+
class_option :path, type: :string, default: nil,
|
|
19
|
+
desc: "Where to put it, e.g. billing/invoice (default: <plural>/show)"
|
|
20
|
+
|
|
21
|
+
desc "Copies a starter template (#{AVAILABLE.join(", ")}) into app/views"
|
|
22
|
+
|
|
23
|
+
def validate_name
|
|
24
|
+
return if AVAILABLE.include?(name)
|
|
25
|
+
|
|
26
|
+
raise ::Rails::Generators::Error,
|
|
27
|
+
"Unknown template #{name.inspect}. Available: #{AVAILABLE.join(", ")}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def copy_template
|
|
31
|
+
copy_file "#{name}/#{name}.typ", "app/views/#{destination}.typ"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def copy_sample_data
|
|
35
|
+
copy_file "#{name}/sample_data.json",
|
|
36
|
+
"app/views/#{File.dirname(destination)}/sample_data.json"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ensure_branding
|
|
40
|
+
return if File.exist?(File.join(destination_root, "app/views/shared/branding.typ"))
|
|
41
|
+
|
|
42
|
+
copy_file "shared/branding.typ", "app/views/shared/branding.typ"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def report
|
|
46
|
+
say ""
|
|
47
|
+
say "Preview it: rake typstify:preview[#{destination}]", :green
|
|
48
|
+
say ""
|
|
49
|
+
say "Render it from a controller:"
|
|
50
|
+
say ""
|
|
51
|
+
say " render pdf: #{destination.inspect},"
|
|
52
|
+
say " data: { ... },"
|
|
53
|
+
say " filename: #{"#{name}.pdf".inspect}"
|
|
54
|
+
say ""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def destination
|
|
60
|
+
@destination ||= begin
|
|
61
|
+
given = options[:path]
|
|
62
|
+
given.nil? || given.empty? ? "#{name}s/show" : given
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :typstify do
|
|
4
|
+
desc "Compile a template with its sample data: rake typstify:preview[invoices/show]"
|
|
5
|
+
task :preview, [:template] do |_task, args|
|
|
6
|
+
require "typstify"
|
|
7
|
+
require "json"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
|
|
10
|
+
name = args[:template]
|
|
11
|
+
abort "Usage: rake typstify:preview[invoices/show]" if name.to_s.empty?
|
|
12
|
+
|
|
13
|
+
resolution = Typstify::Resolver.new.call(name)
|
|
14
|
+
sample = resolution.path.dirname.join("sample_data.json")
|
|
15
|
+
data =
|
|
16
|
+
if sample.file?
|
|
17
|
+
JSON.parse(File.read(sample, encoding: Encoding::UTF_8))
|
|
18
|
+
else
|
|
19
|
+
warn "No sample_data.json next to #{resolution.path}; rendering with empty data."
|
|
20
|
+
{}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
output = Pathname.new("tmp/previews").join("#{name.tr("/", "-")}.pdf")
|
|
24
|
+
FileUtils.mkdir_p(output.dirname)
|
|
25
|
+
output.binwrite(Typstify.render(template: name, data: data))
|
|
26
|
+
|
|
27
|
+
puts "Wrote #{output} (#{output.size} bytes)"
|
|
28
|
+
system("open", output.to_s) if RUBY_PLATFORM.include?("darwin") && ENV["TYPSTIFY_OPEN"] != "0"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "List the templates Typstify can see"
|
|
32
|
+
task :templates do
|
|
33
|
+
require "typstify"
|
|
34
|
+
root = Typstify.config.template_root
|
|
35
|
+
found = Dir.glob(root.join("**", "*.typ{,.erb}"))
|
|
36
|
+
puts "template_root: #{root}"
|
|
37
|
+
if found.empty?
|
|
38
|
+
puts " (none)"
|
|
39
|
+
else
|
|
40
|
+
found.each { |path| puts " #{Pathname.new(path).relative_path_from(root)}" }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|