texd 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -1
- data/Gemfile.lock +1 -1
- data/Makefile +1 -1
- data/README.md +122 -7
- data/gemfiles/rails-6.0.lock +1 -1
- data/gemfiles/rails-6.1.lock +1 -1
- data/gemfiles/rails-7.0.lock +1 -1
- data/lib/texd/attachment.rb +11 -1
- data/lib/texd/cache.rb +103 -0
- data/lib/texd/client.rb +0 -18
- data/lib/texd/config.rb +17 -9
- data/lib/texd/version.rb +1 -1
- data/lib/texd.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f90c7123f60872130f0a2c36a6a6fa44154d1e3956c140f45e07e6815761363
|
4
|
+
data.tar.gz: a1c855d5a76517dae7eb2c5a2a9cc9be621236fa59018e62fbeeaa9b31ee0d49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ecc5d22800aea0f4b77e76c0c052106cb80bb47c3c3a478696c712957fbc178ac74c7014d3927ff09bd45c4472a167fc8ebd7bcbd9ff154f12c90f2724df319d
|
7
|
+
data.tar.gz: 88dad14c7c7f95fbb53bb12bd9777de8033d8b8aceafe896efabcf8f0c33463ece25b90ef43e2e3108c617586007171c498a969fe7184763d73812cf35ed0cf4
|
data/.rubocop.yml
CHANGED
@@ -288,7 +288,9 @@ Style/RedundantAssignment:
|
|
288
288
|
Enabled: true
|
289
289
|
|
290
290
|
Style/RedundantFetchBlock:
|
291
|
-
Enabled:
|
291
|
+
Enabled: false
|
292
|
+
Exclude:
|
293
|
+
- spec/lib/texd/cache_spec.rb # not called on Hash
|
292
294
|
|
293
295
|
Style/RedundantFileExtensionInRequire:
|
294
296
|
Enabled: true
|
data/Gemfile.lock
CHANGED
data/Makefile
CHANGED
data/README.md
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
texd is a Ruby client for the [texd web service](https://github.com/digineo/texd).
|
4
4
|
|
5
|
+
It leverages ActionView's template rendering mechanism to compile `.tex`
|
6
|
+
templates to PDF documents. The primary use case is to render documents
|
7
|
+
in background jobs.
|
8
|
+
|
5
9
|
## Installation
|
6
10
|
|
7
11
|
You need to meet the following requirements for this gem to work:
|
@@ -34,11 +38,15 @@ end
|
|
34
38
|
|
35
39
|
```rb
|
36
40
|
Texd.configure do |config|
|
37
|
-
config.endpoint
|
38
|
-
config.
|
39
|
-
config.
|
40
|
-
config.
|
41
|
-
config.
|
41
|
+
config.endpoint = ENV.fetch("TEXD_ENDPOINT", "http://localhost:2201/")
|
42
|
+
config.open_timeout = ENV.fetch("TEXD_OPEN_TIMEOUT", 60)
|
43
|
+
config.read_timeout = ENV.fetch("TEXD_READ_TIMEOUT", 180)
|
44
|
+
config.write_timeout = ENV.fetch("TEXD_WRITE_TIMEOUT", 60)
|
45
|
+
config.error_format = ENV.fetch("TEXD_ERRORS", "full")
|
46
|
+
config.tex_engine = ENV["TEXD_ENGINE"]
|
47
|
+
config.tex_image = ENV["TEXD_IMAGE"]
|
48
|
+
config.helpers = []
|
49
|
+
config.lookup_paths = [] # Rails.root.join("app/tex") is always inserted as first entry
|
42
50
|
end
|
43
51
|
```
|
44
52
|
|
@@ -56,8 +64,115 @@ about other installation methods.
|
|
56
64
|
|
57
65
|
## Usage
|
58
66
|
|
59
|
-
|
60
|
-
|
67
|
+
First, create a few files:
|
68
|
+
|
69
|
+
<details><summary><code>app/views/layouts/application.tex.erb</code></summary>
|
70
|
+
|
71
|
+
This is the default layout. Here, you should define a `\documentclass`
|
72
|
+
and use `yield`. In this example, we're using ERB (Erubi) to include
|
73
|
+
dynamic content into a `.tex` file.
|
74
|
+
|
75
|
+
```erb
|
76
|
+
\documentclass{article}
|
77
|
+
\usepackage{graphicx}
|
78
|
+
<%= content_for :preamble %>
|
79
|
+
|
80
|
+
\begin{document}
|
81
|
+
<%= yield %>
|
82
|
+
\end{document}
|
83
|
+
```
|
84
|
+
|
85
|
+
</details>
|
86
|
+
<details><summary><code>app/views/document/doc.tex.erb</code></summary>
|
87
|
+
|
88
|
+
In `document/doc.tex`, we're specifying some stuff for the preamble,
|
89
|
+
render a partial, and add content for the document:
|
90
|
+
|
91
|
+
```erb
|
92
|
+
<% content_for :preamble do %>
|
93
|
+
\usepackage{blindtext}
|
94
|
+
|
95
|
+
\title{Demo document}
|
96
|
+
\date{\today}
|
97
|
+
\author{<%= user.full_name %>}
|
98
|
+
<% end %>
|
99
|
+
|
100
|
+
<%= render partial: "document/title_page" %>
|
101
|
+
|
102
|
+
\Blinddocument
|
103
|
+
```
|
104
|
+
|
105
|
+
OK, that wasn't true. We're leveraging the `blindtext` package to add
|
106
|
+
content for us :)
|
107
|
+
|
108
|
+
The `user` variable is passed as local method to `Texd.render` (see below).
|
109
|
+
|
110
|
+
</details>
|
111
|
+
<details><summary><code>app/views/document/_title_page.tex.erb</code></summary>
|
112
|
+
|
113
|
+
This partial embeds an image and creates the title page.
|
114
|
+
|
115
|
+
```erb
|
116
|
+
\begin{center}
|
117
|
+
\includegraphics[width=0.5\linewidth]{<%= texd_attach "logo.png" %>}
|
118
|
+
\end{center}
|
119
|
+
|
120
|
+
\titlepage
|
121
|
+
```
|
122
|
+
|
123
|
+
With `texd_attach`, we're referencing a file *outside* ActionView's lookup
|
124
|
+
paths, but in Texd's lookup paths (`RAILS_ROOT/app/tex` by default).
|
125
|
+
|
126
|
+
You can use this directory to store and deploy static assets.
|
127
|
+
|
128
|
+
Please be aware, that attachments will be renamed (`att00123.png`)
|
129
|
+
in the POST body, and `att00123.png` will be returned from `texd_attach`.
|
130
|
+
You can skip the renaming, if you want/need to:
|
131
|
+
|
132
|
+
```erb
|
133
|
+
% attaches RAILS_ROOT/app/tex/logo.png, and inserts "logo.png":
|
134
|
+
<%= texd_attach "logo.png", rename: false %>
|
135
|
+
|
136
|
+
% attaches RAILS_ROOT/app/tex/logo.png, and inserts "assets/logo.png":
|
137
|
+
<%= texd_attach "logo.png", rename: "assets/logo.png" %>
|
138
|
+
|
139
|
+
% attaches RAILS_ROOT/app/tex/common.tex, and inserts "att00042" (or similar):
|
140
|
+
<%= texd_attach "common.tex", without_extension: true %>
|
141
|
+
```
|
142
|
+
|
143
|
+
</details>
|
144
|
+
<details><summary><code>app/tex/logo.png</code></summary>
|
145
|
+
|
146
|
+
*(Imagine your logo here.)*
|
147
|
+
|
148
|
+
</details>
|
149
|
+
|
150
|
+
With those files in place, you can create a PDF document:
|
151
|
+
|
152
|
+
```rb
|
153
|
+
begin
|
154
|
+
blob = Texd.render(template: "documents/doc", locals: {
|
155
|
+
user: User.find(1)
|
156
|
+
})
|
157
|
+
Rails.root.join("tmp/doc.pdf").open("wb") { |f|
|
158
|
+
f.write blob
|
159
|
+
}
|
160
|
+
rescue Texd::Client::QueueError => err
|
161
|
+
# texd server is busy, please retry in a moment
|
162
|
+
rescue Texd::Client::InputError => err
|
163
|
+
# file input processing failed, maybe some file names were invalid
|
164
|
+
rescue Texd::Client::CompilationError => err
|
165
|
+
# compilation failed
|
166
|
+
if err.logs
|
167
|
+
# TeX compiler logs, only available if Texd.config.error_format
|
168
|
+
# is "full" or "condensed"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
All errors inherit from `Texd::Client::RenderError` and should have
|
174
|
+
a `details` attribute (a Hash) containing the actual error returned
|
175
|
+
from the server.
|
61
176
|
|
62
177
|
## Development
|
63
178
|
|
data/gemfiles/rails-6.0.lock
CHANGED
data/gemfiles/rails-6.1.lock
CHANGED
data/gemfiles/rails-7.0.lock
CHANGED
data/lib/texd/attachment.rb
CHANGED
@@ -202,9 +202,19 @@ module Texd
|
|
202
202
|
}
|
203
203
|
end
|
204
204
|
|
205
|
+
def self.cache
|
206
|
+
@cache ||= Cache.new(Texd.config.ref_cache_size)
|
207
|
+
end
|
208
|
+
|
205
209
|
# @api private
|
206
210
|
def checksum
|
207
|
-
@checksum ||=
|
211
|
+
@checksum ||= create_checksum
|
212
|
+
end
|
213
|
+
|
214
|
+
# @api private
|
215
|
+
def create_checksum
|
216
|
+
key = [::File.stat(absolute_path).mtime, absolute_path]
|
217
|
+
self.class.cache.fetch(key) do
|
208
218
|
digest = Digest::SHA256.file(absolute_path).digest
|
209
219
|
encoded = Base64.urlsafe_encode64(digest)
|
210
220
|
"sha256:#{encoded}"
|
data/lib/texd/cache.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Texd
|
4
|
+
# Cache is a simple LRU cache with a double linked list.
|
5
|
+
# If the cache is full the last element from the list will be removed.
|
6
|
+
class Cache
|
7
|
+
class LinkedList
|
8
|
+
attr_reader :head
|
9
|
+
|
10
|
+
def add(node)
|
11
|
+
if @head
|
12
|
+
node.next = @head
|
13
|
+
node.prev = @head.prev
|
14
|
+
@head.prev.next = node
|
15
|
+
@head.prev = node
|
16
|
+
else
|
17
|
+
node.next = node
|
18
|
+
node.prev = node
|
19
|
+
end
|
20
|
+
|
21
|
+
@head = node
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove(node)
|
25
|
+
if node.next == node
|
26
|
+
# remove last element, should never happen
|
27
|
+
@head = nil
|
28
|
+
else
|
29
|
+
node.prev.next = node.next
|
30
|
+
node.next.prev = node.prev
|
31
|
+
@head = node.next if node == @head
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Node
|
37
|
+
attr_reader :key
|
38
|
+
attr_accessor :value, :prev, :next
|
39
|
+
|
40
|
+
def initialize(key)
|
41
|
+
@key = key
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :list, :count, :capacity
|
46
|
+
delegate :keys, to: :@hash
|
47
|
+
|
48
|
+
def initialize(capacity)
|
49
|
+
@list = LinkedList.new
|
50
|
+
@hash = {}
|
51
|
+
@mtx = Mutex.new
|
52
|
+
@count = 0
|
53
|
+
@capacity = capacity
|
54
|
+
end
|
55
|
+
|
56
|
+
def fetch(key)
|
57
|
+
read(key) || write(key, yield)
|
58
|
+
end
|
59
|
+
|
60
|
+
def read(key)
|
61
|
+
@mtx.synchronize do
|
62
|
+
node = @hash[key]
|
63
|
+
return unless node
|
64
|
+
|
65
|
+
list.remove(node)
|
66
|
+
list.add(node)
|
67
|
+
node.value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def write(key, value)
|
72
|
+
@mtx.synchronize do
|
73
|
+
node = @hash[key]
|
74
|
+
|
75
|
+
if node
|
76
|
+
list.remove(node)
|
77
|
+
else
|
78
|
+
node = add_node(key)
|
79
|
+
end
|
80
|
+
|
81
|
+
list.add(node)
|
82
|
+
node.value = value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def add_node(key)
|
89
|
+
node = Node.new(key)
|
90
|
+
|
91
|
+
if count >= capacity
|
92
|
+
last = list.head.prev
|
93
|
+
@hash.delete(last.key)
|
94
|
+
list.remove(last)
|
95
|
+
else
|
96
|
+
@count += 1
|
97
|
+
end
|
98
|
+
|
99
|
+
@hash[key] = node
|
100
|
+
node
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/texd/client.rb
CHANGED
@@ -60,24 +60,6 @@ module Texd
|
|
60
60
|
"reference" => ReferenceError,
|
61
61
|
}.freeze
|
62
62
|
|
63
|
-
class ResponseError < Error
|
64
|
-
attr_reader :body, :content_type
|
65
|
-
|
66
|
-
def initialize(code, content_type, body)
|
67
|
-
@body = body
|
68
|
-
@content_type = content_type
|
69
|
-
|
70
|
-
if json?
|
71
|
-
super format("%s error: %s", body.delete("category"), body.delete("error"))
|
72
|
-
elsif log?
|
73
|
-
tex_errors = body.lines.select { |l| l.start_with?("!") }.map(&:strip)
|
74
|
-
super "Compilation failed:\n\t#{tex_errors.join('\n\t')}"
|
75
|
-
else
|
76
|
-
super "Server responded with status #{code} (#{content_type})"
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
63
|
USER_AGENT = "texd-ruby/#{VERSION} Ruby/#{RUBY_VERSION}"
|
82
64
|
|
83
65
|
attr_reader :config
|
data/lib/texd/config.rb
CHANGED
@@ -22,15 +22,16 @@ module Texd
|
|
22
22
|
|
23
23
|
# This is the default configuration. It is applied in the constructor.
|
24
24
|
DEFAULT_CONFIGURATION = {
|
25
|
-
endpoint:
|
26
|
-
open_timeout:
|
27
|
-
read_timeout:
|
28
|
-
write_timeout:
|
29
|
-
error_format:
|
30
|
-
tex_engine:
|
31
|
-
tex_image:
|
32
|
-
helpers:
|
33
|
-
lookup_paths:
|
25
|
+
endpoint: ENV.fetch("TEXD_ENDPOINT", "http://localhost:2201/"),
|
26
|
+
open_timeout: ENV.fetch("TEXD_OPEN_TIMEOUT", 60),
|
27
|
+
read_timeout: ENV.fetch("TEXD_READ_TIMEOUT", 180),
|
28
|
+
write_timeout: ENV.fetch("TEXD_WRITE_TIMEOUT", 60),
|
29
|
+
error_format: ENV.fetch("TEXD_ERRORS", "full"),
|
30
|
+
tex_engine: ENV["TEXD_ENGINE"],
|
31
|
+
tex_image: ENV["TEXD_IMAGE"],
|
32
|
+
helpers: Set.new,
|
33
|
+
lookup_paths: [], # Rails.root.join("app/tex") is inserted in railtie.rb
|
34
|
+
ref_cache_size: 128,
|
34
35
|
}.freeze
|
35
36
|
|
36
37
|
# Supported endpoint protocols.
|
@@ -107,6 +108,13 @@ module Texd
|
|
107
108
|
# A Texd::LookupContext is constructed from this set.
|
108
109
|
attr_accessor :lookup_paths
|
109
110
|
|
111
|
+
# Cache size for file hashes computed by Texd::Attachment::Reference.
|
112
|
+
# Cannot be changed after the first document (using the `texd_reference`
|
113
|
+
# helper) was renderered.
|
114
|
+
#
|
115
|
+
# By default, the cache keeps hashes of the last 128 reference files.
|
116
|
+
attr_accessor :ref_cache_size
|
117
|
+
|
110
118
|
def initialize(**options)
|
111
119
|
DEFAULT_CONFIGURATION.each do |key, default_value|
|
112
120
|
public_send "#{key}=", options.fetch(key, default_value.dup)
|
data/lib/texd/version.rb
CHANGED
data/lib/texd.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: texd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dominik Menke
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-03-
|
11
|
+
date: 2022-03-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multipart-post
|
@@ -125,6 +125,7 @@ files:
|
|
125
125
|
- gemfiles/rails-7.0.lock
|
126
126
|
- lib/texd.rb
|
127
127
|
- lib/texd/attachment.rb
|
128
|
+
- lib/texd/cache.rb
|
128
129
|
- lib/texd/client.rb
|
129
130
|
- lib/texd/config.rb
|
130
131
|
- lib/texd/document.rb
|
@@ -140,7 +141,7 @@ metadata:
|
|
140
141
|
rubygems_mfa_required: 'true'
|
141
142
|
homepage_uri: https://github.com/digineo/texd-ruby
|
142
143
|
source_code_uri: https://github.com/digineo/texd-ruby
|
143
|
-
changelog_uri: https://github.com/digineo/texd-ruby/blob/v0.
|
144
|
+
changelog_uri: https://github.com/digineo/texd-ruby/blob/v0.3.0/CHANGELOG.md
|
144
145
|
post_install_message:
|
145
146
|
rdoc_options: []
|
146
147
|
require_paths:
|