texd 0.2.2 → 0.3.2

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: 1ee764a1a8120ae0b54ad110cfc3ba25a2ca921a5fd624c3fa5da036b2fb3c5d
4
+ data.tar.gz: 54da08cc7ff4177e845afcfa384139935eaee421b14e5ec2827ef7a94f91a3ad
5
5
  SHA512:
6
- metadata.gz: 398a048ca70ecdb2a65ec3466ea900ebd918f7aacc5d4335ea1099cacf9342729a5e232eaed8af9bed261d4ca1e6d6ceb1e19476acea5ccc5a310b1520157afc
7
- data.tar.gz: 7dd01397f2fdc0975dc0e31d8dcc2fb8de21017af585615333ad34a62ffcbce1793e994f6f6ac25eefa70c79779b8edfa1f5fd3d831b581ce826613b477b05c1
6
+ metadata.gz: 57df5731df9356a484732266af41022dfe39fec86890e2f4983fac495827713d2f81a85e31318ff103f72c0dff5266d42c9c9f4bfcf7faf1014ed2c776ec2254
7
+ data.tar.gz: 3f9e4d4d9f14a993099a509903d33e58f16756f55f9c647968c0f628ceeaed96ce76f1cea30244128919ccd89774ec60289c61eaded6a0e4608636374a53638d
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.2)
5
5
  multipart-post (~> 2.0)
6
6
  rails (>= 6.0, < 8)
7
7
 
@@ -119,7 +119,7 @@ GEM
119
119
  nio4r (2.5.8)
120
120
  nokogiri (1.13.3-x86_64-linux)
121
121
  racc (~> 1.4)
122
- parallel (1.22.0)
122
+ parallel (1.22.1)
123
123
  parser (3.1.1.0)
124
124
  ast (~> 2.4.1)
125
125
  pry (0.13.1)
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.2)
4
+ texd (0.3.2)
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.2)
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.2)
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
@@ -105,6 +87,7 @@ module Texd
105
87
 
106
88
  Net::HTTP.start uri.host, uri.port, **request_options(uri) do |http|
107
89
  req = yield(uri)
90
+ req.basic_auth(uri.user, uri.password) if uri.user || uri.password
108
91
  decode_response http.request(req)
109
92
  end
110
93
  end
@@ -126,7 +109,7 @@ module Texd
126
109
  end
127
110
 
128
111
  def decode_response(res)
129
- ct = res["Content-Type"]
112
+ ct = res["Content-Type"].to_s
130
113
  body = case ct.split(";").first
131
114
  when "application/json"
132
115
  JSON.parse(res.body)
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(template:, locals: {})
10
- new.compile(template: template, locals: locals)
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.2"
4
+ VERSION = "0.3.2"
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.2
4
+ version: 0.3.2
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-28 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.2/CHANGELOG.md
144
145
  post_install_message:
145
146
  rdoc_options: []
146
147
  require_paths: