texd 0.2.1 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c54956eeb977294ed946d8f961d16c15e41eff3eca556d31787174c07852de97
4
- data.tar.gz: cd603c3d2e9af5b04c43a8c0f9e542fddd18b815a5a01c3d8483f3729b8dd9f1
3
+ metadata.gz: 6ac1013a4aa1ebd3b6889f40a3de4b8e48b6b55ea5413f1c997141c3660d8c81
4
+ data.tar.gz: 5fd52ec7b93abdd53c12276977b13e8c00ca8d16af39659be19128a3e9684860
5
5
  SHA512:
6
- metadata.gz: 979a9503f9e4c38817d0c54b42c0ae288592819a65d0db1bd9ccf0dcaad81005d27842f35b49fc9b7869bfbb8563817e5e447413a865499d93993b95e0749081
7
- data.tar.gz: a9d55ce2394c5bf940360ef4f11c7524c50149a0989da9a4c62ee186ecf6fddfcdba695a753ae9f20db20d55720179acecb4e9f5d6f08cc4dab4ec1f8850a81d
6
+ metadata.gz: 6522bec685edb067867df07b39b9de12d554b9907c2886e6f72fd53377a9dc5dc87cdc6370f4af4067970aa65b4f4a62415f7eb46aec65dadd1c9f3d65badfae
7
+ data.tar.gz: 29283c3b8a723f156ae5ce67f79e0cedd2c186b940a831eb7a7fe8a972f50ab5b1bb29017137edbfa87668429f190f5a82bc883b97299c6b8f783c5a678fb944
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.1)
4
+ texd (0.3.1)
5
5
  multipart-post (~> 2.0)
6
6
  rails (>= 6.0, < 8)
7
7
 
data/Makefile CHANGED
@@ -45,10 +45,11 @@ docs:
45
45
  .PHONY: texd-docker
46
46
  texd-docker:
47
47
  mkdir -p tmp/jobs tmp/refs
48
+ rm -vf tmp/jobs/* tmp/refs/*
48
49
  docker run --rm \
49
50
  --name texd-dev \
50
51
  -p 127.0.0.1:2201:2201 \
51
52
  -v $$(pwd)/tmp/jobs:/texd \
52
53
  -v $$(pwd)/tmp/refs:/refs \
53
- --user=$(id -u):$(id -g) \
54
- digineode/texd --reference-store dir:///refs
54
+ -u $$(id -u):$$(id -g) \
55
+ digineode/texd --reference-store dir:///refs --keep-jobs always
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.1)
4
+ texd (0.3.1)
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.1)
4
+ texd (0.3.1)
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.1)
4
+ texd (0.3.1)
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/document.rb CHANGED
@@ -6,8 +6,8 @@ module Texd
6
6
  attr_reader :attachments
7
7
 
8
8
  # Shorthand for `new.compile`.
9
- def self.compile(*args)
10
- new.compile(*args)
9
+ def self.compile(**kwargs)
10
+ new.compile(**kwargs)
11
11
  end
12
12
 
13
13
  def initialize
@@ -22,12 +22,15 @@ module Texd
22
22
  # context.
23
23
  # @param [Hash, nil] locals will be made available as getter methods in
24
24
  # the template.
25
+ # @param [String, Boolean] layout to be used. String value name template
26
+ # files in `app/views/layouts`, `true` (default) uses the application
27
+ # layout, and `false` renders without a layout.
25
28
  # @return [Compilation]
26
- def compile(template:, locals: {})
29
+ def compile(template:, locals: {}, layout: true)
27
30
  helper_mod = ::Texd.helpers(attachments, locals)
28
31
  tex_source = Class.new(ApplicationController) {
29
32
  helper helper_mod
30
- }.render(template: template, format: :tex)
33
+ }.render(template: template, format: :tex, layout: layout)
31
34
 
32
35
  main = attachments.main_input(tex_source)
33
36
  Compilation.new(main.name, attachments)
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.1"
4
+ VERSION = "0.3.1"
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"
@@ -118,12 +119,15 @@ module Texd
118
119
  # context.
119
120
  # @param [Hash, nil] locals will be made available as getter methods in
120
121
  # the template.
122
+ # @param [String, Boolean] layout to be used. String value name template
123
+ # files in `app/views/layouts`, `true` (default) uses the application
124
+ # layout, and `false` renders without a layout.
121
125
  # @raise [Texd::Client::ResponseError] on input and queue errors. Also on
122
126
  # compilation errors, if Texd.config.error_format is set to JSON.
123
127
  # @raise [Texd::Error] on other Texd related errors.
124
128
  # @return [String] the PDF object
125
- def render(template:, locals: {})
126
- doc = Document.compile(template: template, locals: locals)
129
+ def render(template:, locals: {}, layout: true)
130
+ doc = Document.compile(template: template, locals: locals, layout: layout)
127
131
 
128
132
  client.render doc.to_upload_ios,
129
133
  input: doc.main_input_name
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.1
4
+ version: 0.3.1
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.1/CHANGELOG.md
144
+ changelog_uri: https://github.com/digineo/texd-ruby/blob/v0.3.1/CHANGELOG.md
144
145
  post_install_message:
145
146
  rdoc_options: []
146
147
  require_paths: