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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33d8cf48e8c880611c853f282bc3db47cf5d627022887dd311ff7a895a3f24e9
4
- data.tar.gz: c77ab1247262c025cd814d58b22f32f21f241a33fb5d95b15d1d805409546534
3
+ metadata.gz: 8f90c7123f60872130f0a2c36a6a6fa44154d1e3956c140f45e07e6815761363
4
+ data.tar.gz: a1c855d5a76517dae7eb2c5a2a9cc9be621236fa59018e62fbeeaa9b31ee0d49
5
5
  SHA512:
6
- metadata.gz: 398a048ca70ecdb2a65ec3466ea900ebd918f7aacc5d4335ea1099cacf9342729a5e232eaed8af9bed261d4ca1e6d6ceb1e19476acea5ccc5a310b1520157afc
7
- data.tar.gz: 7dd01397f2fdc0975dc0e31d8dcc2fb8de21017af585615333ad34a62ffcbce1793e994f6f6ac25eefa70c79779b8edfa1f5fd3d831b581ce826613b477b05c1
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: true
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- texd (0.2.2)
4
+ texd (0.3.0)
5
5
  multipart-post (~> 2.0)
6
6
  rails (>= 6.0, < 8)
7
7
 
data/Makefile CHANGED
@@ -50,5 +50,5 @@ texd-docker:
50
50
  -p 127.0.0.1:2201:2201 \
51
51
  -v $$(pwd)/tmp/jobs:/texd \
52
52
  -v $$(pwd)/tmp/refs:/refs \
53
- --user=$(id -u):$(id -g) \
53
+ --user=$$(id -u):$$(id -g) \
54
54
  digineode/texd --reference-store dir:///refs
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 = ENV.fetch("TEXD_ENDPOINT", "http://localhost:2201/")
38
- config.error_format = ENV.fetch("TEXD_ERRORS", "full")
39
- config.tex_engine = ENV["TEXD_ENGINE"]
40
- config.tex_image = ENV["TEXD_IMAGE"]
41
- config.helpers = []
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
- > TODO. See https://github.com/amagical-net/rails-latex#label-Synopsis
60
- > in the meantime.
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
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- texd (0.2.2)
4
+ texd (0.3.0)
5
5
  multipart-post (~> 2.0)
6
6
  rails (>= 6.0, < 8)
7
7
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- texd (0.2.2)
4
+ texd (0.3.0)
5
5
  multipart-post (~> 2.0)
6
6
  rails (>= 6.0, < 8)
7
7
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- texd (0.2.2)
4
+ texd (0.3.0)
5
5
  multipart-post (~> 2.0)
6
6
  rails (>= 6.0, < 8)
7
7
 
@@ -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 ||= begin
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: 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
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Texd
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/texd.rb CHANGED
@@ -12,6 +12,7 @@ require "rails"
12
12
  require_relative "texd/version"
13
13
  require_relative "texd/config"
14
14
  require_relative "texd/helpers"
15
+ require_relative "texd/cache"
15
16
  require_relative "texd/client"
16
17
  require_relative "texd/attachment"
17
18
  require_relative "texd/document"
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.2.2
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-22 00:00:00.000000000 Z
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.2.2/CHANGELOG.md
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: