latexml-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9f12825ac7bc96cc0bcde6d9d4b10cd303760873
4
+ data.tar.gz: 832c989f6b175c6cc0ad3e45d489cbe547ecdb83
5
+ SHA512:
6
+ metadata.gz: 6b69ee41107db0eb159a9c1a7a412a64f8497c75e74402d9bd9acfe4b2d9b822f115359921ebabe8d889b05ec570a16b3ced06e130d77820b66947f8b0686c32
7
+ data.tar.gz: dc5aafc5be83e8e305dee9b6d646647faf426607c98650bc51e07820d4ff3863d2b85c94b4d5631dd0c3557d9105ce43c52b84e2f5484ffd1758feda455db174
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *~
data/.travis.yml ADDED
@@ -0,0 +1,21 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.4
5
+ addons:
6
+ apt:
7
+ packages:
8
+ - libdb-dev
9
+ - libxml2-dev
10
+ - libxslt1-dev
11
+ - libgdbm-dev
12
+ before_install:
13
+ - gem install bundler -v 1.12.5
14
+ # Thanks to SO: http://stackoverflow.com/a/32358866
15
+ # Install modules into ~/perl5 using system perl
16
+ - curl -L https://cpanmin.us | perl - App::cpanminus
17
+ - export PATH=$PATH:~/perl5/bin/
18
+ - cpanm --local-lib=~/perl5 local::lib && eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
19
+ - cpanm JSON
20
+ - cpanm --notest https://github.com/brucemiller/LaTeXML.git
21
+ - cpanm https://github.com/dginev/LaTeXML-Plugin-latexmls.git
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in latexml-ruby.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Deyan Ginev
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # LaTeXML-Ruby
2
+
3
+ [![Build Status](https://secure.travis-ci.org/Authorea/latexml-ruby.png?branch=master)](https://travis-ci.org/Authorea/latexml-ruby)
4
+ [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/authorea/LaTeXML-Ruby/master/LICENSE)
5
+
6
+
7
+ A Ruby wrapper for the [LaTeXML](http://dlmf.nist.gov/LaTeXML/) LaTeX to XML/HTML/ePub converter.
8
+
9
+ Includes support for daemonized conversion runs, for additional performance, via the [latexmls](https://github.com/dginev/LaTeXML-Plugin-latexmls) socket server.
10
+
11
+ ## Why LaTeXML?
12
+
13
+ You may be familiar with other LaTeX conversion tools such as Pandoc or tex4ht. LaTeXML attempts to be a complete TeX interpreter, and covers a vastly larger range of the TeX/LaTeX ecosystem than Pandoc. At the same time it allows for just-in-time binding of structural and semantic macros, which allows it to create higher quality HTML5 than tex4ht, and makes bridging the impedance mismatch between PDF and HTML an achievable goal.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'latexml-ruby'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install latexml-ruby
30
+
31
+ ## Usage
32
+
33
+ A hello world conversion job looks like:
34
+
35
+ ```ruby
36
+ @latexml = LaTeXML.new
37
+
38
+ response = @latexml.convert(literal: "hello world")
39
+
40
+ result = response[:result]
41
+ messages = response[:messages]
42
+
43
+ ```
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Authorea/latexml-ruby.
48
+
49
+ The 0.0.1 release of the wrapper brings support for easy conversion of latex fragments, which only scratches the surface of LaTeXML's versatile conversion use cases. If you are interested in a different workflow that is not yet supported, we will be very happy to hear from you.
50
+
51
+ ## License
52
+
53
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
54
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ Gem::Specification.new do |spec|
3
+ spec.name = "latexml-ruby"
4
+ spec.version = "0.0.1"
5
+
6
+ spec.authors = ["Deyan Ginev"]
7
+ spec.email = ["deyan@authorea.com"]
8
+
9
+ spec.summary = %q{Ruby wrapper for LaTeXML}
10
+ spec.description = %q{The wrapper automates LaTeX to HTML5 conversions with LaTeXML, addressing common production needs such as error-handling, timeouts, managing option sets and automatic recognition of available binaries.}
11
+ spec.homepage = "https://github.com/Authorea/latexml-ruby"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
15
+ spec.require_paths = ["lib"]
16
+
17
+ spec.add_dependency 'escape_utils', '~> 1.2'
18
+ spec.add_dependency 'json', '~> 1.8'
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.12"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "minitest", "~> 5.0"
23
+ spec.add_development_dependency "minitest-reporters", "~> 1.1"
24
+
25
+ end
data/lib/latexml.rb ADDED
@@ -0,0 +1,270 @@
1
+ class LaTeXML
2
+ require 'socket'
3
+ require 'timeout'
4
+ require 'net/http'
5
+ require 'escape_utils'
6
+ require 'json'
7
+
8
+ def initialize(options = {})
9
+ # Timeouts are tricky with this setup.
10
+ # For example, we know a regular Authorea latexml job should be done in 12 seconds,
11
+ # but sometimes we boot a new latexmls server, which takes about 5-6 seconds total, so we end up with
12
+ # http requests that may take up to 18 seconds. Requires setup-specific tuning.
13
+ options = {debug: true, preload_timeout: 6, latexml_timeout: 12, timeout_rescue_sleep: 0.5,
14
+ setup: [
15
+ {expire: 86400},
16
+ {autoflush: 10000},
17
+ {cache_key: 'latexml_ruby'}, # Cache this setup, for avoiding the startup runtime costs
18
+ {nocomments: true},
19
+ {nographicimages: true},
20
+ {nopictureimages: true},
21
+ {noparse: true}, # Don't parse the math, using MathJaX for now
22
+ {format: 'html5'},
23
+ {nodefaultresources: true}, # Don't copy any aux files over
24
+ {whatsin: 'fragment'},
25
+ {whatsout: 'fragment'},
26
+ # TeX preloads:
27
+ # The more preloads are provided on initialization, the faster the conversion overall
28
+ # NOTE: LaTeXML will gracefully handle repeated \usepackage loads of the same package, so better add more preloads
29
+ # than worry about conflicts
30
+ %w(article.cls graphicx.sty latexsym.sty amsfonts.sty amsmath.sty amsthm.sty
31
+ amstext.sty amssymb.sty eucal.sty [utf8]inputenc.sty url.sty hyperref.sty textcomp.sty longtable.sty
32
+ multirow.sty booktabs.sty fixltx2e.sty
33
+ fullpage.sty [table,dvipsnames]xcolor.sty listings.sty deluxetable.sty xspace.sty
34
+ [noids]latexml.sty [labels]lxRDFa.sty
35
+ secureio.sty) # This is crucial for security reasons.
36
+ .collect{|style| {preload: style} }].flatten
37
+ }.merge(options)
38
+ @debug = options[:debug]
39
+ @preload_timeout = options[:preload_timeout]
40
+
41
+ @latexml_timeout = options[:latexml_timeout]
42
+ options[:setup].push({timeout: @latexml_timeout.to_s}) # also pass to latexml
43
+
44
+ @http_timeout = @latexml_timeout + @preload_timeout
45
+ @timeout_rescue_sleep = options[:timeout_rescue_sleep]
46
+ # The current set of default options has historically been used for converting Authorea content
47
+ # with LaTeXML 0.8.1 and up
48
+
49
+ # Note that we need an array of hashes, because duplicate keys ARE allowed (e.g. path)
50
+ # and more importantly, the ORDER of options is meaningful (overrides are also allowed)
51
+ @setup_options = options[:setup]
52
+
53
+ @response_server_unreachable = options[:response_server_unreachable] || {
54
+ result: '',
55
+ log:[{
56
+ severity: 'fatal',
57
+ category: 'latexmls',
58
+ what: 'server unreachable',
59
+ details: "The LaTeXML server was unreachable at this time"}]
60
+ }
61
+
62
+ @response_connection_reset = options[:response_connection_reset] || {
63
+ result: '',
64
+ log:[{
65
+ severity: 'fatal',
66
+ category: 'latexmls',
67
+ what: 'connection reset',
68
+ details: "The LaTeXML server was unreachable at this time"}]
69
+ }
70
+
71
+ @response_empty_input = options[:response_empty_input] || {
72
+ result: '',
73
+ log:[{severity: 'no_problem'}]
74
+ }
75
+ end
76
+
77
+ def convert(options={})
78
+ source = options.delete(:literal) || options.delete(:source)
79
+ if source.to_s.strip.empty?
80
+ return @response_empty_input.deep_dup
81
+ end
82
+ source = "literal:#{source}"
83
+
84
+ render_options = [{source: EscapeUtils.escape_uri(source)}]
85
+ if !options[:preamble].to_s.strip.empty?
86
+ render_options << {preamble: EscapeUtils.escape_uri(options[:preamble])}
87
+ end
88
+ # Discussion: This could be useful if/when we decide to have IDs in the document fragments of an article
89
+ # However, at the moment it is hitting a flaw in the LaTeXML design which causes all packages to reload on every conversion
90
+ # and that is SLOW. So commenting out for now.
91
+ # if options[:documentid].present?
92
+ # render_options << {documentid: EscapeUtils.escape_uri(options[:documentid])}'"
93
+ # end
94
+
95
+ render_options.concat @setup_options
96
+
97
+ time_before_call = Time.now
98
+ # We are talking to socket servers, via the LaTeXML-Plugin-latexmls extension:
99
+ server_port = options[:server_port] || 3334
100
+ server_address = options[:server_address] || "0.0.0.0"
101
+ # We can only proceed if we have a working socket server
102
+ if !ensure_latexmls(server_port)
103
+ return @response_server_unreachable.deep_dup
104
+ end
105
+
106
+ # Setting up POST request
107
+ post_body = render_options.map{|h| h.map{|k,v| (v == true) ? k : "#{k}=#{v}"}}.flatten.join("&")
108
+ latexmls_uri = URI.parse("http://#{server_address}:#{server_port}")
109
+ request = Net::HTTP::Post.new(latexmls_uri.request_uri)
110
+ request.body = post_body
111
+ request['Content-Type'] = 'application/x-www-form-urlencoded'
112
+ http = Net::HTTP.new(latexmls_uri.host, latexmls_uri.port)
113
+ http.read_timeout = @http_timeout # give up after X seconds
114
+
115
+ puts "*** Starting LaTeXML call to port #{server_port}" if @debug
116
+ http_response = nil
117
+ begin
118
+ Timeout::timeout(@http_timeout) do # we'll keep trying for X seconds before giving up
119
+ # we are going to retry on failure, as this is likely an autoflush process reboot (expected behaviour)
120
+ loop do
121
+ begin
122
+ http_response = http.request(request)
123
+ break
124
+ rescue => e
125
+ puts "*** latexmls http request error: #{e.message}" if @debug
126
+ sleep @timeout_rescue_sleep #avoid DoS
127
+ if !ensure_latexmls(server_port)
128
+ response = @response_connection_reset.deep_dup
129
+ if e && e.message
130
+ response[:what] = e.message
131
+ end
132
+ return response
133
+ end
134
+ end
135
+ end
136
+ end
137
+ rescue Timeout::Error
138
+ if @debug
139
+ latexml_time = Time.now - time_before_call
140
+ puts "LATEXML: Timeout took #{latexml_time} seconds"
141
+ puts "LATEXML: request: #{request.body}"
142
+ end
143
+ return @response_connection_reset.deep_dup
144
+ end
145
+ latexml_time = Time.now - time_before_call
146
+ puts "*** LaTeXML call to port #{server_port} took #{latexml_time} seconds" if @debug
147
+
148
+ if @debug && (latexml_time > 5.0)
149
+ puts "LATEXML: Slow render took #{latexml_time} seconds"
150
+ puts "LATEXML: request: #{request.body}"
151
+ # email_subject = "LATEXML: Slow render took #{latexml_time} seconds"
152
+ # email_content = "#{request.body}"
153
+ # Resque.enqueue(NotificationsWorker, email_subject, email_content)
154
+ end
155
+
156
+ response = JSON.parse(http_response.body)
157
+
158
+ html = response["result"] || ""
159
+ log = response["log"] || ""
160
+ # if html.to_s.strip.empty?
161
+ # puts "LATEXML: Empty result"
162
+ # puts "LATEXML: request: #{request.body}"
163
+ # email_subject = "LATEXML: Empty result"
164
+ # email_content = "#{request.body}\n\nLog: #{log}"
165
+ # Resque.enqueue(NotificationsWorker, email_subject, email_content)
166
+ # end
167
+ # We can check for the error code if we want to: 0 is ok, 1 is warning, 2 is error and 3 is fatal error
168
+ # status = response["status"]
169
+
170
+ # Return the HTML content:
171
+ return {result: html, messages: parse_log(log)}
172
+ end
173
+
174
+ # Parses a log string which follows the LaTeXML convention
175
+ # (described at http://dlmf.nist.gov/LaTeXML/manual/errorcodes/index.html)
176
+ def parse_log(content)
177
+ # Quit unless we have some data
178
+ content = content.to_s.strip
179
+ return if content.empty?
180
+ # Obtain the individual lines
181
+ messages = []
182
+ in_details_mode = false
183
+ content.split("\n").reject{|l| l.to_s.strip.empty?}.each do |line|
184
+ # If we have found a message header and we're collecting details:
185
+ if in_details_mode
186
+ # If the line starts with tab, we are indeed reading in details
187
+ if line.match(/^\t/)
188
+ # Append details line to the last message"
189
+ messages.last[:details].concat("\n#{line}")
190
+ if messages.last[:line].to_s.strip.empty? # Only get the first line#col report
191
+ if posmatch = line.match(/at Literal String(.*); line (\d+) col (\d+)/)
192
+ messages.last[:line] = posmatch[2]
193
+ messages.last[:col] = posmatch[3]
194
+ end
195
+ end
196
+ next # This line has been consumed, next
197
+ else
198
+ in_details_mode = false
199
+ # Not a details line, continue the current iteration with analyzing a new message
200
+ end
201
+ end
202
+
203
+ # Since this isn't a details line, check if it's a message line:
204
+ if matches = line.match(/^([^ :]+)\:([^ :]+)\:([^ ]+)(\s(.+))?$/)
205
+ # Indeed a message, so record it:
206
+ message = {severity: matches[1].downcase, category: matches[2].downcase, what: matches[3].downcase, details: matches[5] ? matches[5].downcase : ''}
207
+ # Prepare to record follow-up lines with the message details:
208
+ in_details_mode = true
209
+ # Add to the array of parsed messages
210
+ messages.push(message)
211
+ else
212
+ # Otherwise line is just noise, continue...
213
+ in_details_mode = false
214
+ end
215
+ end
216
+ # Return the parsed messages
217
+ return messages
218
+ end
219
+
220
+ # One way of checking if we have a socket server running at a given port
221
+ def local_port_open?(port, seconds=1)
222
+ Timeout::timeout(seconds) do
223
+ begin
224
+ TCPSocket.new('localhost', port).close
225
+ true
226
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
227
+ false
228
+ end
229
+ end
230
+ rescue Timeout::Error
231
+ false
232
+ end
233
+
234
+ def ensure_latexmls(server_port = 3334)
235
+ Timeout::timeout(@preload_timeout) do # we'll try for X seconds before giving up
236
+ loop do
237
+ if local_port_open?(server_port)
238
+ break
239
+ else
240
+ # expire=600: daemonized, if idle for 10 minutes will self-terminate
241
+ # expire=86400: daemonized, if idle for 24 hours will self-terminate
242
+ # autoflush: auto-restart process after X conversions. Useful if memory is leaking (shouldn't be). 0 to disable.
243
+ sys_options = @setup_options.map do |h|
244
+ h.map do |k,v|
245
+ (v==true) ? ["--#{k}"] : ["--#{k}", v.to_s]
246
+ end
247
+ end.flatten
248
+ system(LaTeXML.executable,"--port",server_port.to_s, *sys_options)
249
+ end
250
+ end
251
+ return true
252
+ end
253
+ rescue Timeout::Error
254
+ return false
255
+ end
256
+
257
+ def self.is_installed?
258
+ self.executable
259
+ end
260
+
261
+ def self.executable
262
+ @@executable ||= if system("which latexmls > /dev/null 2>&1")
263
+ 'latexmls'
264
+ elsif system("which latexmlc > /dev/null 2>&1")
265
+ 'latexmlc'
266
+ else
267
+ nil
268
+ end
269
+ end
270
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: latexml-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Deyan Ginev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: escape_utils
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest-reporters
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.1'
97
+ description: The wrapper automates LaTeX to HTML5 conversions with LaTeXML, addressing
98
+ common production needs such as error-handling, timeouts, managing option sets and
99
+ automatic recognition of available binaries.
100
+ email:
101
+ - deyan@authorea.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".travis.yml"
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - latexml-ruby.gemspec
113
+ - lib/latexml.rb
114
+ homepage: https://github.com/Authorea/latexml-ruby
115
+ licenses:
116
+ - MIT
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.4.8
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Ruby wrapper for LaTeXML
138
+ test_files: []