docverter 0.0.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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in docverter-api.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Pete Keen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Docverter
2
+
3
+ This is the official Docverter Ruby API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'docverter-api'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install docverter-api
18
+
19
+ ## Usage
20
+
21
+ First, sign up for [Docverter](http://www.docverter.com) and get your API key.
22
+
23
+ Set your API key before using any of the rest of the library:
24
+
25
+ Docverter.api_key = <API-KEY>
26
+
27
+ In a Rails project, put this in an initializer.
28
+
29
+ A few example conversions:
30
+
31
+ Docverter::Conversion.run("markdown", "html", "Some Content")
32
+ # => "<html><body><p>Some Content</p></body></html>"
33
+
34
+ Docverter::Conversion.run do |c|
35
+ c.from = "markdown"
36
+ c.to = "html"
37
+ c.content = "Some Content"
38
+ c.stylesheet = "stylesheet.css"
39
+ c.add_other_file "stylesheet.css"
40
+ end
41
+ # => '<html><head><link rel="stylesheet" media="print" href="stylesheet.css"></head><body><p>Some Content</p></body></html>'
42
+
43
+ See the documentation for `Docverter::Conversion` for more details.
44
+
45
+ ## Contributing
46
+
47
+ 1. Fork it
48
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
49
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
50
+ 4. Push to the branch (`git push origin my-new-feature`)
51
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :test do
4
+ ret = true
5
+ Dir["test/**/*.rb"].each do |f|
6
+ ret = ret && sh("ruby #{f}")
7
+ end
8
+ exit(ret)
9
+ end
data/docverter.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'docverter/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "docverter"
8
+ gem.version = Docverter::VERSION
9
+ gem.authors = ["Pete Keen"]
10
+ gem.email = ["pete@docverter.com"]
11
+ gem.description = %q{API for converting documents with the Docverter service}
12
+ gem.summary = %q{API for converting documents with the Docverter service}
13
+ gem.homepage = "http://www.docverter.com"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency("rest-client", ["~>1.6.7"])
21
+ gem.add_development_dependency("mocha")
22
+ gem.add_development_dependency("shoulda")
23
+ end
data/lib/docverter.rb ADDED
@@ -0,0 +1,107 @@
1
+ require 'rest_client'
2
+ require 'docverter/json'
3
+ require "docverter/version"
4
+ require "docverter/conversion"
5
+
6
+ module Docverter
7
+
8
+ class AuthenticationError < StandardError; end
9
+ class APIConnectionError < StandardError; end
10
+ class APIError < StandardError; end
11
+
12
+ @@api_key = nil
13
+ @@base_url = "https://api.docverter.com/v1"
14
+
15
+ def self.api_key
16
+ @@api_key
17
+ end
18
+
19
+ def self.api_key=(key)
20
+ @@api_key = key
21
+ end
22
+
23
+ def self.api_url(path='')
24
+ u = URI(@@base_url + path)
25
+ key = self.api_key
26
+ raise AuthenticationError.new('No API key provided. (HINT: set your API key using "Docverter.api_key = <API-KEY>". You can find your API in the Docverter web interface. See http://www.docverter.com/api.html for details, or email pete@docverter.com if you have any questions.)') unless key
27
+ u.user = key
28
+ u.password = ''
29
+ u.to_s
30
+ end
31
+
32
+ def self.base_url=(url)
33
+ @@base_url = url
34
+ end
35
+
36
+ def self.reset
37
+ @@api_key = nil
38
+ @@override_url = nil
39
+ end
40
+
41
+ def self.request(method, url, params={}, headers={})
42
+ key = @@api_key
43
+ raise AuthenticationERror.new('No API key provided. (HINT: set your API key using "Docverter.api_key = <API-KEY>". You can find your API in the Docverter web interface. See http://www.docverter.com/api.html for details, or email pete@docverter.com if you have any questions.)') unless key
44
+
45
+ url = self.api_url(url)
46
+
47
+ headers = {
48
+ :user_agent => "Docverter/v1 RubyBindings/#{Docverter::VERSION}",
49
+ :content_type => "multipart/form-data"
50
+ }.merge(headers)
51
+
52
+ opts = {
53
+ :method => method,
54
+ :url => url,
55
+ :headers => headers,
56
+ :open_timeout => 30,
57
+ :payload => params,
58
+ }
59
+
60
+ begin
61
+ response = execute_request(opts)
62
+ rescue SocketError => e
63
+ self.handle_restclient_error(e)
64
+ rescue NoMethodError => e
65
+ # Work around RestClient bug
66
+ if e.message =~ /\WRequestFailed\W/
67
+ e = APIConnectionError.new('Unexpected HTTP response code')
68
+ self.handle_restclient_error(e)
69
+ else
70
+ raise
71
+ end
72
+ rescue RestClient::ExceptionWithResponse => e
73
+ if rcode = e.http_code and rbody = e.http_body
74
+ self.handle_api_error(rcode, rbody)
75
+ else
76
+ self.handle_restclient_error(e)
77
+ end
78
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
79
+ self.handle_restclient_error(e)
80
+ end
81
+
82
+ response.body
83
+ end
84
+
85
+ def self.execute_request(opts)
86
+ RestClient::Request.execute(opts)
87
+ end
88
+
89
+ def self.handle_api_error(code, body)
90
+ obj = Docverter::OkJson.decode(body)
91
+ raise APIError.new("Docverter API Error: #{obj['error']} (status: #{code})")
92
+ end
93
+
94
+ def self.handle_restclient_error(e)
95
+ case e
96
+ when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
97
+ message = "Could not connect to Docverter (#{@@api_base}). Please check your internet connection and try again. If this problem persists, you should let me know at pete@docverter.com."
98
+ when SocketError
99
+ message = "Unexpected error communicating when trying to connect to Docverter. HINT: You may be seeing this message because your DNS is not working. To check, try running 'host docverter.com' from the command line."
100
+ else
101
+ message = "Unexpected error communicating with Docverter. If this problem persists, let me know at pete@docverter.com."
102
+ end
103
+ message += "\n\n(Network error: #{e.message})"
104
+ raise APIConnectionError.new(message)
105
+ end
106
+ end
107
+
@@ -0,0 +1,142 @@
1
+ require 'ostruct'
2
+
3
+ module Docverter
4
+
5
+ # Public: An invalid conversion has been specified
6
+ class InvalidConversionError < StandardError; end
7
+
8
+ # Public: Convert documents using Docverter
9
+ #
10
+ # Examples
11
+ #
12
+ # Docverter::Conversion.run("markdown", "html", "Some Content")
13
+ # # => "<html><body><p>Some Content</p></body></html>"
14
+ #
15
+ # Docverter::Conversion.run do |c|
16
+ # c.from = "markdown"
17
+ # c.to = "html"
18
+ # c.content = "Some Content"
19
+ # c.stylesheet = "stylesheet.css"
20
+ # c.add_other_file "stylesheet.css"
21
+ # end
22
+ # # => '<html><head><link rel="stylesheet" media="print" href="stylesheet.css"></head><body><p>Some Content</p></body></html>'
23
+ #
24
+ class Conversion < OpenStruct
25
+
26
+ # Public: Run a conversion on Docverter
27
+ #
28
+ # from - The format of the input text (optional)
29
+ # to - The format of the output document (optional)
30
+ # content - The content to be converted (optional)
31
+ #
32
+ # Yields the initialized Conversion object
33
+ #
34
+ # Returns the converted document or a status hash if a callback_url has been specified
35
+ #
36
+ # Examples
37
+ #
38
+ # Docverter::Conversion.run("markdown", "pdf", "Some Content")
39
+ #
40
+ # Docverter::Conversion.run do |c|
41
+ # c.from = "markdown"
42
+ # c.to = "pdf"
43
+ # c.add_input_file("input.md")
44
+ # c.stylesheet = "stylesheet.css"
45
+ # c.add_other_file "stylesheet.css"
46
+ # end
47
+ def self.run(from=nil, to=nil, content=nil)
48
+ obj = new(from, to, content)
49
+
50
+ if block_given?
51
+ yield obj
52
+ end
53
+
54
+ obj.convert
55
+ end
56
+
57
+ # Public: Get the status of a particular conversion
58
+ #
59
+ # id - The integer ID of a conversion as given by Docverter::Conversion#run in async mode
60
+ #
61
+ # Returns a status hash
62
+ def self.status(id)
63
+ OkJson.decode(Docverter.request(:get, "/status/#{id.to_i}"))
64
+ end
65
+
66
+ # Public: Pick up the converted document for a particular conversion
67
+ #
68
+ # id - The integer ID of a conversion as given by Docverter::Conversion#run in async mode
69
+ #
70
+ # Returns the converted document
71
+ def self.pickup(id)
72
+ Docverter.request(:get, "/pickup/#{id.to_i}")
73
+ end
74
+
75
+ # Public: Run a conversion on Docverter
76
+ #
77
+ # from - The format of the input text (optional)
78
+ # to - The format of the output document (optional)
79
+ # content - The content to be converted (optional)
80
+ #
81
+ # Can accept a block that will be passed the initialized Conversion object to allow
82
+ # more settings to be set before the conversion is run
83
+ #
84
+ # Returns a new Conversion object
85
+ def initialize(from=nil, to=nil, content=nil)
86
+ super(
87
+ :from => from,
88
+ :to => to,
89
+ :content => content,
90
+ :input_files => [],
91
+ :other_files => [],
92
+ )
93
+ end
94
+
95
+ # Public: Add an input file to the conversion
96
+ #
97
+ # path - The path to the file to add as an input file
98
+ def add_input_file(path)
99
+ self.input_files << File.open(path, "rb")
100
+ end
101
+
102
+ # Public: Add another file to the conversion
103
+ #
104
+ # path - The path to the file to add
105
+ def add_other_file(path)
106
+ self.other_files << File.open(path, "rb")
107
+ end
108
+
109
+ # Public: Run the conversion through Docverter
110
+ def convert
111
+
112
+ if input_files.length == 0 && content == nil
113
+ raise InvalidConversionError.new("Cannot convert without an input_file or content")
114
+ end
115
+
116
+ unless self.content.nil?
117
+ temp = Tempfile.new("temp")
118
+ temp.write self.content
119
+ temp.flush
120
+ self.add_input_file(temp.path)
121
+ end
122
+
123
+ response = Docverter.request(:post, "/convert", self.to_h)
124
+ if self.callback_url
125
+ OkJson.decode(response)
126
+ else
127
+ response
128
+ end
129
+ end
130
+
131
+ # Internal: Convert this conversion into a hash
132
+ def to_h
133
+ hash = {}
134
+ self.instance_variable_get(:@table).each do |field,value|
135
+ next if value.nil?
136
+ hash[field] = value
137
+ end
138
+
139
+ hash
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,598 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2011, 2012 Keith Rarick
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.
22
+
23
+ # See https://github.com/kr/okjson for updates.
24
+
25
+ require 'stringio'
26
+
27
+ # Some parts adapted from
28
+ # http://golang.org/src/pkg/json/decode.go and
29
+ # http://golang.org/src/pkg/utf8/utf8.go
30
+ module Docverter
31
+ module OkJson
32
+ extend self
33
+
34
+
35
+ # Decodes a json document in string s and
36
+ # returns the corresponding ruby value.
37
+ # String s must be valid UTF-8. If you have
38
+ # a string in some other encoding, convert
39
+ # it first.
40
+ #
41
+ # String values in the resulting structure
42
+ # will be UTF-8.
43
+ def decode(s)
44
+ ts = lex(s)
45
+ v, ts = textparse(ts)
46
+ if ts.length > 0
47
+ raise Error, 'trailing garbage'
48
+ end
49
+ v
50
+ end
51
+
52
+
53
+ # Parses a "json text" in the sense of RFC 4627.
54
+ # Returns the parsed value and any trailing tokens.
55
+ # Note: this is almost the same as valparse,
56
+ # except that it does not accept atomic values.
57
+ def textparse(ts)
58
+ if ts.length < 0
59
+ raise Error, 'empty'
60
+ end
61
+
62
+ typ, _, val = ts[0]
63
+ case typ
64
+ when '{' then objparse(ts)
65
+ when '[' then arrparse(ts)
66
+ else
67
+ raise Error, "unexpected #{val.inspect}"
68
+ end
69
+ end
70
+
71
+
72
+ # Parses a "value" in the sense of RFC 4627.
73
+ # Returns the parsed value and any trailing tokens.
74
+ def valparse(ts)
75
+ if ts.length < 0
76
+ raise Error, 'empty'
77
+ end
78
+
79
+ typ, _, val = ts[0]
80
+ case typ
81
+ when '{' then objparse(ts)
82
+ when '[' then arrparse(ts)
83
+ when :val,:str then [val, ts[1..-1]]
84
+ else
85
+ raise Error, "unexpected #{val.inspect}"
86
+ end
87
+ end
88
+
89
+
90
+ # Parses an "object" in the sense of RFC 4627.
91
+ # Returns the parsed value and any trailing tokens.
92
+ def objparse(ts)
93
+ ts = eat('{', ts)
94
+ obj = {}
95
+
96
+ if ts[0][0] == '}'
97
+ return obj, ts[1..-1]
98
+ end
99
+
100
+ k, v, ts = pairparse(ts)
101
+ obj[k] = v
102
+
103
+ if ts[0][0] == '}'
104
+ return obj, ts[1..-1]
105
+ end
106
+
107
+ loop do
108
+ ts = eat(',', ts)
109
+
110
+ k, v, ts = pairparse(ts)
111
+ obj[k] = v
112
+
113
+ if ts[0][0] == '}'
114
+ return obj, ts[1..-1]
115
+ end
116
+ end
117
+ end
118
+
119
+
120
+ # Parses a "member" in the sense of RFC 4627.
121
+ # Returns the parsed values and any trailing tokens.
122
+ def pairparse(ts)
123
+ (typ, _, k), ts = ts[0], ts[1..-1]
124
+ if typ != :str
125
+ raise Error, "unexpected #{k.inspect}"
126
+ end
127
+ ts = eat(':', ts)
128
+ v, ts = valparse(ts)
129
+ [k, v, ts]
130
+ end
131
+
132
+
133
+ # Parses an "array" in the sense of RFC 4627.
134
+ # Returns the parsed value and any trailing tokens.
135
+ def arrparse(ts)
136
+ ts = eat('[', ts)
137
+ arr = []
138
+
139
+ if ts[0][0] == ']'
140
+ return arr, ts[1..-1]
141
+ end
142
+
143
+ v, ts = valparse(ts)
144
+ arr << v
145
+
146
+ if ts[0][0] == ']'
147
+ return arr, ts[1..-1]
148
+ end
149
+
150
+ loop do
151
+ ts = eat(',', ts)
152
+
153
+ v, ts = valparse(ts)
154
+ arr << v
155
+
156
+ if ts[0][0] == ']'
157
+ return arr, ts[1..-1]
158
+ end
159
+ end
160
+ end
161
+
162
+
163
+ def eat(typ, ts)
164
+ if ts[0][0] != typ
165
+ raise Error, "expected #{typ} (got #{ts[0].inspect})"
166
+ end
167
+ ts[1..-1]
168
+ end
169
+
170
+
171
+ # Scans s and returns a list of json tokens,
172
+ # excluding white space (as defined in RFC 4627).
173
+ def lex(s)
174
+ ts = []
175
+ while s.length > 0
176
+ typ, lexeme, val = tok(s)
177
+ if typ == nil
178
+ raise Error, "invalid character at #{s[0,10].inspect}"
179
+ end
180
+ if typ != :space
181
+ ts << [typ, lexeme, val]
182
+ end
183
+ s = s[lexeme.length..-1]
184
+ end
185
+ ts
186
+ end
187
+
188
+
189
+ # Scans the first token in s and
190
+ # returns a 3-element list, or nil
191
+ # if s does not begin with a valid token.
192
+ #
193
+ # The first list element is one of
194
+ # '{', '}', ':', ',', '[', ']',
195
+ # :val, :str, and :space.
196
+ #
197
+ # The second element is the lexeme.
198
+ #
199
+ # The third element is the value of the
200
+ # token for :val and :str, otherwise
201
+ # it is the lexeme.
202
+ def tok(s)
203
+ case s[0]
204
+ when ?{ then ['{', s[0,1], s[0,1]]
205
+ when ?} then ['}', s[0,1], s[0,1]]
206
+ when ?: then [':', s[0,1], s[0,1]]
207
+ when ?, then [',', s[0,1], s[0,1]]
208
+ when ?[ then ['[', s[0,1], s[0,1]]
209
+ when ?] then [']', s[0,1], s[0,1]]
210
+ when ?n then nulltok(s)
211
+ when ?t then truetok(s)
212
+ when ?f then falsetok(s)
213
+ when ?" then strtok(s)
214
+ when Spc then [:space, s[0,1], s[0,1]]
215
+ when ?\t then [:space, s[0,1], s[0,1]]
216
+ when ?\n then [:space, s[0,1], s[0,1]]
217
+ when ?\r then [:space, s[0,1], s[0,1]]
218
+ else numtok(s)
219
+ end
220
+ end
221
+
222
+
223
+ def nulltok(s); s[0,4] == 'null' ? [:val, 'null', nil] : [] end
224
+ def truetok(s); s[0,4] == 'true' ? [:val, 'true', true] : [] end
225
+ def falsetok(s); s[0,5] == 'false' ? [:val, 'false', false] : [] end
226
+
227
+
228
+ def numtok(s)
229
+ m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s)
230
+ if m && m.begin(0) == 0
231
+ if m[3] && !m[2]
232
+ [:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))]
233
+ elsif m[2]
234
+ [:val, m[0], Float(m[0])]
235
+ else
236
+ [:val, m[0], Integer(m[0])]
237
+ end
238
+ else
239
+ []
240
+ end
241
+ end
242
+
243
+
244
+ def strtok(s)
245
+ m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s)
246
+ if ! m
247
+ raise Error, "invalid string literal at #{abbrev(s)}"
248
+ end
249
+ [:str, m[0], unquote(m[0])]
250
+ end
251
+
252
+
253
+ def abbrev(s)
254
+ t = s[0,10]
255
+ p = t['`']
256
+ t = t[0,p] if p
257
+ t = t + '...' if t.length < s.length
258
+ '`' + t + '`'
259
+ end
260
+
261
+
262
+ # Converts a quoted json string literal q into a UTF-8-encoded string.
263
+ # The rules are different than for Ruby, so we cannot use eval.
264
+ # Unquote will raise an error if q contains control characters.
265
+ def unquote(q)
266
+ q = q[1...-1]
267
+ a = q.dup # allocate a big enough string
268
+ rubydoesenc = false
269
+ # In ruby >= 1.9, a[w] is a codepoint, not a byte.
270
+ if a.class.method_defined?(:force_encoding)
271
+ a.force_encoding('UTF-8')
272
+ rubydoesenc = true
273
+ end
274
+ r, w = 0, 0
275
+ while r < q.length
276
+ c = q[r]
277
+ case true
278
+ when c == ?\\
279
+ r += 1
280
+ if r >= q.length
281
+ raise Error, "string literal ends with a \"\\\": \"#{q}\""
282
+ end
283
+
284
+ case q[r]
285
+ when ?",?\\,?/,?'
286
+ a[w] = q[r]
287
+ r += 1
288
+ w += 1
289
+ when ?b,?f,?n,?r,?t
290
+ a[w] = Unesc[q[r]]
291
+ r += 1
292
+ w += 1
293
+ when ?u
294
+ r += 1
295
+ uchar = begin
296
+ hexdec4(q[r,4])
297
+ rescue RuntimeError => e
298
+ raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}"
299
+ end
300
+ r += 4
301
+ if surrogate? uchar
302
+ if q.length >= r+6
303
+ uchar1 = hexdec4(q[r+2,4])
304
+ uchar = subst(uchar, uchar1)
305
+ if uchar != Ucharerr
306
+ # A valid pair; consume.
307
+ r += 6
308
+ end
309
+ end
310
+ end
311
+ if rubydoesenc
312
+ a[w] = '' << uchar
313
+ w += 1
314
+ else
315
+ w += ucharenc(a, w, uchar)
316
+ end
317
+ else
318
+ raise Error, "invalid escape char #{q[r]} in \"#{q}\""
319
+ end
320
+ when c == ?", c < Spc
321
+ raise Error, "invalid character in string literal \"#{q}\""
322
+ else
323
+ # Copy anything else byte-for-byte.
324
+ # Valid UTF-8 will remain valid UTF-8.
325
+ # Invalid UTF-8 will remain invalid UTF-8.
326
+ # In ruby >= 1.9, c is a codepoint, not a byte,
327
+ # in which case this is still what we want.
328
+ a[w] = c
329
+ r += 1
330
+ w += 1
331
+ end
332
+ end
333
+ a[0,w]
334
+ end
335
+
336
+
337
+ # Encodes unicode character u as UTF-8
338
+ # bytes in string a at position i.
339
+ # Returns the number of bytes written.
340
+ def ucharenc(a, i, u)
341
+ case true
342
+ when u <= Uchar1max
343
+ a[i] = (u & 0xff).chr
344
+ 1
345
+ when u <= Uchar2max
346
+ a[i+0] = (Utag2 | ((u>>6)&0xff)).chr
347
+ a[i+1] = (Utagx | (u&Umaskx)).chr
348
+ 2
349
+ when u <= Uchar3max
350
+ a[i+0] = (Utag3 | ((u>>12)&0xff)).chr
351
+ a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr
352
+ a[i+2] = (Utagx | (u&Umaskx)).chr
353
+ 3
354
+ else
355
+ a[i+0] = (Utag4 | ((u>>18)&0xff)).chr
356
+ a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr
357
+ a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr
358
+ a[i+3] = (Utagx | (u&Umaskx)).chr
359
+ 4
360
+ end
361
+ end
362
+
363
+
364
+ def hexdec4(s)
365
+ if s.length != 4
366
+ raise Error, 'short'
367
+ end
368
+ (nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3])
369
+ end
370
+
371
+
372
+ def subst(u1, u2)
373
+ if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3
374
+ return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself
375
+ end
376
+ return Ucharerr
377
+ end
378
+
379
+
380
+ def surrogate?(u)
381
+ Usurr1 <= u && u < Usurr3
382
+ end
383
+
384
+
385
+ def nibble(c)
386
+ case true
387
+ when ?0 <= c && c <= ?9 then c.ord - ?0.ord
388
+ when ?a <= c && c <= ?z then c.ord - ?a.ord + 10
389
+ when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10
390
+ else
391
+ raise Error, "invalid hex code #{c}"
392
+ end
393
+ end
394
+
395
+
396
+ # Encodes x into a json text. It may contain only
397
+ # Array, Hash, String, Numeric, true, false, nil.
398
+ # (Note, this list excludes Symbol.)
399
+ # X itself must be an Array or a Hash.
400
+ # No other value can be encoded, and an error will
401
+ # be raised if x contains any other value, such as
402
+ # Nan, Infinity, Symbol, and Proc, or if a Hash key
403
+ # is not a String.
404
+ # Strings contained in x must be valid UTF-8.
405
+ def encode(x)
406
+ case x
407
+ when Hash then objenc(x)
408
+ when Array then arrenc(x)
409
+ else
410
+ raise Error, 'root value must be an Array or a Hash'
411
+ end
412
+ end
413
+
414
+
415
+ def valenc(x)
416
+ case x
417
+ when Hash then objenc(x)
418
+ when Array then arrenc(x)
419
+ when String then strenc(x)
420
+ when Numeric then numenc(x)
421
+ when true then "true"
422
+ when false then "false"
423
+ when nil then "null"
424
+ else
425
+ raise Error, "cannot encode #{x.class}: #{x.inspect}"
426
+ end
427
+ end
428
+
429
+
430
+ def objenc(x)
431
+ '{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}'
432
+ end
433
+
434
+
435
+ def arrenc(a)
436
+ '[' + a.map{|x| valenc(x)}.join(',') + ']'
437
+ end
438
+
439
+
440
+ def keyenc(k)
441
+ case k
442
+ when String then strenc(k)
443
+ else
444
+ raise Error, "Hash key is not a string: #{k.inspect}"
445
+ end
446
+ end
447
+
448
+
449
+ def strenc(s)
450
+ t = StringIO.new
451
+ t.putc(?")
452
+ r = 0
453
+
454
+ # In ruby >= 1.9, s[r] is a codepoint, not a byte.
455
+ rubydoesenc = s.class.method_defined?(:encoding)
456
+
457
+ while r < s.length
458
+ case s[r]
459
+ when ?" then t.print('\\"')
460
+ when ?\\ then t.print('\\\\')
461
+ when ?\b then t.print('\\b')
462
+ when ?\f then t.print('\\f')
463
+ when ?\n then t.print('\\n')
464
+ when ?\r then t.print('\\r')
465
+ when ?\t then t.print('\\t')
466
+ else
467
+ c = s[r]
468
+ case true
469
+ when rubydoesenc
470
+ begin
471
+ c.ord # will raise an error if c is invalid UTF-8
472
+ t.write(c)
473
+ rescue
474
+ t.write(Ustrerr)
475
+ end
476
+ when Spc <= c && c <= ?~
477
+ t.putc(c)
478
+ else
479
+ n = ucharcopy(t, s, r) # ensure valid UTF-8 output
480
+ r += n - 1 # r is incremented below
481
+ end
482
+ end
483
+ r += 1
484
+ end
485
+ t.putc(?")
486
+ t.string
487
+ end
488
+
489
+
490
+ def numenc(x)
491
+ if ((x.nan? || x.infinite?) rescue false)
492
+ raise Error, "Numeric cannot be represented: #{x}"
493
+ end
494
+ "#{x}"
495
+ end
496
+
497
+
498
+ # Copies the valid UTF-8 bytes of a single character
499
+ # from string s at position i to I/O object t, and
500
+ # returns the number of bytes copied.
501
+ # If no valid UTF-8 char exists at position i,
502
+ # ucharcopy writes Ustrerr and returns 1.
503
+ def ucharcopy(t, s, i)
504
+ n = s.length - i
505
+ raise Utf8Error if n < 1
506
+
507
+ c0 = s[i].ord
508
+
509
+ # 1-byte, 7-bit sequence?
510
+ if c0 < Utagx
511
+ t.putc(c0)
512
+ return 1
513
+ end
514
+
515
+ raise Utf8Error if c0 < Utag2 # unexpected continuation byte?
516
+
517
+ raise Utf8Error if n < 2 # need continuation byte
518
+ c1 = s[i+1].ord
519
+ raise Utf8Error if c1 < Utagx || Utag2 <= c1
520
+
521
+ # 2-byte, 11-bit sequence?
522
+ if c0 < Utag3
523
+ raise Utf8Error if ((c0&Umask2)<<6 | (c1&Umaskx)) <= Uchar1max
524
+ t.putc(c0)
525
+ t.putc(c1)
526
+ return 2
527
+ end
528
+
529
+ # need second continuation byte
530
+ raise Utf8Error if n < 3
531
+
532
+ c2 = s[i+2].ord
533
+ raise Utf8Error if c2 < Utagx || Utag2 <= c2
534
+
535
+ # 3-byte, 16-bit sequence?
536
+ if c0 < Utag4
537
+ u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx)
538
+ raise Utf8Error if u <= Uchar2max
539
+ t.putc(c0)
540
+ t.putc(c1)
541
+ t.putc(c2)
542
+ return 3
543
+ end
544
+
545
+ # need third continuation byte
546
+ raise Utf8Error if n < 4
547
+ c3 = s[i+3].ord
548
+ raise Utf8Error if c3 < Utagx || Utag2 <= c3
549
+
550
+ # 4-byte, 21-bit sequence?
551
+ if c0 < Utag5
552
+ u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx)
553
+ raise Utf8Error if u <= Uchar3max
554
+ t.putc(c0)
555
+ t.putc(c1)
556
+ t.putc(c2)
557
+ t.putc(c3)
558
+ return 4
559
+ end
560
+
561
+ raise Utf8Error
562
+ rescue Utf8Error
563
+ t.write(Ustrerr)
564
+ return 1
565
+ end
566
+
567
+
568
+ class Utf8Error < ::StandardError
569
+ end
570
+
571
+
572
+ class Error < ::StandardError
573
+ end
574
+
575
+
576
+ Utagx = 0x80 # 1000 0000
577
+ Utag2 = 0xc0 # 1100 0000
578
+ Utag3 = 0xe0 # 1110 0000
579
+ Utag4 = 0xf0 # 1111 0000
580
+ Utag5 = 0xF8 # 1111 1000
581
+ Umaskx = 0x3f # 0011 1111
582
+ Umask2 = 0x1f # 0001 1111
583
+ Umask3 = 0x0f # 0000 1111
584
+ Umask4 = 0x07 # 0000 0111
585
+ Uchar1max = (1<<7) - 1
586
+ Uchar2max = (1<<11) - 1
587
+ Uchar3max = (1<<16) - 1
588
+ Ucharerr = 0xFFFD # unicode "replacement char"
589
+ Ustrerr = "\xef\xbf\xbd" # unicode "replacement char"
590
+ Usurrself = 0x10000
591
+ Usurr1 = 0xd800
592
+ Usurr2 = 0xdc00
593
+ Usurr3 = 0xe000
594
+
595
+ Spc = ' '[0]
596
+ Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t}
597
+ end
598
+ end
@@ -0,0 +1,3 @@
1
+ module Docverter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,164 @@
1
+ require File.expand_path("../test_helper", __FILE__)
2
+
3
+ class TestDocverter < Test::Unit::TestCase
4
+
5
+ def setup
6
+ Docverter.reset
7
+ @mock = mock
8
+ Docverter.mock_rest_client = @mock
9
+ Docverter.api_key = "test_key"
10
+ end
11
+
12
+ should "set api key" do
13
+ Docverter.api_key = "key"
14
+ assert_equal "key", Docverter.api_key
15
+ end
16
+
17
+ should "raise if no api key given" do
18
+ Docverter.api_key = nil
19
+ assert_raises(Docverter::AuthenticationError) do
20
+ Docverter.api_url
21
+ end
22
+ end
23
+
24
+ should "have default base url" do
25
+ assert_equal "https://test_key:@api.docverter.com/v1", Docverter.api_url
26
+ end
27
+
28
+ should "override base url" do
29
+ Docverter.base_url = "http://localhost:5000/v1"
30
+ assert_equal "http://test_key:@localhost:5000/v1", Docverter.api_url
31
+ end
32
+
33
+ context "Conversion" do
34
+ should "initialize with from, to, and content" do
35
+ conversion = Docverter::Conversion.new('markdown', 'html', 'content')
36
+ assert_equal "markdown", conversion.from
37
+ assert_equal "html", conversion.to
38
+ assert_equal "content", conversion.content
39
+ assert_equal [], conversion.input_files
40
+ assert_equal [], conversion.other_files
41
+ end
42
+
43
+ should "add an input file from a path" do
44
+
45
+ temp = Tempfile.new("foo")
46
+
47
+ conversion = Docverter::Conversion.new
48
+ conversion.add_input_file(temp.path)
49
+
50
+ assert_equal conversion.input_files[0].path, temp.path
51
+ end
52
+
53
+ should "add an other file from a path" do
54
+ temp = Tempfile.new("foo")
55
+
56
+ conversion = Docverter::Conversion.new
57
+ conversion.add_other_file(temp.path)
58
+
59
+ assert_equal conversion.other_files[0].path, temp.path
60
+ end
61
+
62
+ should "to_h" do
63
+ conversion = Docverter::Conversion.new("markdown", "pdf")
64
+
65
+ h = {
66
+ :from => "markdown",
67
+ :to => "pdf",
68
+ :input_files => [],
69
+ :other_files => [],
70
+ }
71
+
72
+ assert_equal h, conversion.to_h
73
+ end
74
+
75
+ should "make request" do
76
+ temp = Tempfile.new("foo")
77
+
78
+ @mock.expects(:post).with do |url, blah, params|
79
+ url == "https://test_key:@api.docverter.com/v1/convert" \
80
+ && params[:from] == 'markdown' \
81
+ && params[:to] == 'pdf' \
82
+ && params[:other_files] == [] \
83
+ && params[:input_files][0].path == temp.path
84
+ end.returns(test_response("blah"))
85
+
86
+ res = Docverter::Conversion.run do |c|
87
+ c.from = "markdown"
88
+ c.to = "pdf"
89
+ c.add_input_file(temp.path)
90
+ end
91
+
92
+
93
+ assert_equal "blah", res
94
+ end
95
+
96
+ should "make request with content" do
97
+ @mock.expects(:post).with do |url, blah, params|
98
+ url == "https://test_key:@api.docverter.com/v1/convert" \
99
+ && params[:from] == 'markdown' \
100
+ && params[:to] == 'pdf' \
101
+ && params[:other_files] == [] \
102
+ && params[:input_files][0].read == "Some Content"
103
+ end.returns(test_response("Some Content"))
104
+
105
+ res = Docverter::Conversion.run do |c|
106
+ c.from = "markdown"
107
+ c.to = "pdf"
108
+ c.content = "Some Content"
109
+ end
110
+
111
+ assert_equal "Some Content", res
112
+ end
113
+
114
+ should "make request with no input_files or content raises" do
115
+
116
+ assert_raises(Docverter::InvalidConversionError) do
117
+ Docverter::Conversion.run do |c|
118
+ c.from = "markdown"
119
+ c.to = "pdf"
120
+ end
121
+ end
122
+ end
123
+
124
+ should "make async request" do
125
+ temp = Tempfile.new("foo")
126
+
127
+ @mock.expects(:post).with do |url, blah, params|
128
+ url == "https://test_key:@api.docverter.com/v1/convert" \
129
+ && params[:from] == 'markdown' \
130
+ && params[:to] == 'pdf' \
131
+ && params[:other_files] == [] \
132
+ && params[:input_files][0].path == temp.path \
133
+ && params[:callback_url] == 'http://www.google.com'
134
+ end.returns(test_response('{"status": "pending", "id": 123}'))
135
+
136
+ res = Docverter::Conversion.run do |c|
137
+ c.from = "markdown"
138
+ c.to = "pdf"
139
+ c.callback_url = 'http://www.google.com'
140
+ c.add_input_file(temp.path)
141
+
142
+ end
143
+
144
+ expected = {'id' => 123, 'status' => 'pending'}
145
+
146
+ assert_equal expected, res
147
+ end
148
+
149
+ should "pickup" do
150
+ @mock.expects(:get).with("https://test_key:@api.docverter.com/v1/pickup/1", nil, {}).returns(test_response("foo"))
151
+ res = Docverter::Conversion.pickup(1)
152
+ assert_equal "foo", res
153
+ end
154
+
155
+ should "status" do
156
+ @mock.expects(:get).with("https://test_key:@api.docverter.com/v1/status/1", nil, {}).returns(test_response('{"status": "pending", "id": 1}'))
157
+ res = Docverter::Conversion.status(1)
158
+ expected = {"status" => "pending", "id" => 1}
159
+ assert_equal expected, res
160
+ end
161
+
162
+ end
163
+
164
+ end
@@ -0,0 +1,33 @@
1
+ lib = File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'docverter'
4
+ require 'test/unit'
5
+ require 'shoulda'
6
+ require 'mocha'
7
+ require 'tempfile'
8
+
9
+ module Docverter
10
+ @mock_rest_client = nil
11
+
12
+ def self.mock_rest_client=(mock_client)
13
+ @mock_rest_client = mock_client
14
+ end
15
+
16
+ def self.execute_request(opts)
17
+ get_params = (opts[:headers] || {})[:params]
18
+ post_params = opts[:payload]
19
+ case opts[:method]
20
+ when :get then @mock_rest_client.get opts[:url], get_params, post_params
21
+ when :post then @mock_rest_client.post opts[:url], get_params, post_params
22
+ when :delete then @mock_rest_client.delete opts[:url], get_params, post_params
23
+ end
24
+ end
25
+ end
26
+
27
+ def test_response(body, code=200)
28
+ m = mock
29
+ m.instance_variable_set('@docverter_values', { :body => body, :code => code })
30
+ def m.body; @docverter_values[:body]; end
31
+ def m.code; @docverter_values[:code]; end
32
+ m
33
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: docverter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pete Keen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.6.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.6.7
30
+ - !ruby/object:Gem::Dependency
31
+ name: mocha
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: shoulda
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: API for converting documents with the Docverter service
63
+ email:
64
+ - pete@docverter.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE.txt
72
+ - README.md
73
+ - Rakefile
74
+ - docverter.gemspec
75
+ - lib/docverter.rb
76
+ - lib/docverter/conversion.rb
77
+ - lib/docverter/json.rb
78
+ - lib/docverter/version.rb
79
+ - test/test_docverter.rb
80
+ - test/test_helper.rb
81
+ homepage: http://www.docverter.com
82
+ licenses: []
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 1.8.24
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: API for converting documents with the Docverter service
105
+ test_files:
106
+ - test/test_docverter.rb
107
+ - test/test_helper.rb
108
+ has_rdoc: