wineole 0.1.0 → 0.2.0
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 +4 -4
- data/bin/wineole-vba +45 -0
- data/lib/wineole/client.rb +454 -20
- data/lib/wineole/dispatcher.rb +534 -0
- data/lib/wineole/errors.rb +6 -0
- data/lib/wineole/events.rb +353 -0
- data/lib/wineole/msoffice/address.rb +67 -0
- data/lib/wineole/msoffice/book.rb +114 -0
- data/lib/wineole/msoffice/color.rb +87 -0
- data/lib/wineole/msoffice/controls.rb +450 -0
- data/lib/wineole/msoffice/excel.rb +311 -0
- data/lib/wineole/msoffice/format.rb +362 -0
- data/lib/wineole/msoffice/forms.rb +163 -0
- data/lib/wineole/msoffice/passthrough.rb +46 -0
- data/lib/wineole/msoffice/paths.rb +62 -0
- data/lib/wineole/msoffice/range.rb +149 -0
- data/lib/wineole/msoffice/sheet.rb +101 -0
- data/lib/wineole/msoffice/vba.rb +199 -0
- data/lib/wineole/msoffice/vba_api.rb +366 -0
- data/lib/wineole/msoffice/vba_block.rb +144 -0
- data/lib/wineole/msoffice.rb +28 -0
- data/lib/wineole/proxy.rb +126 -15
- data/lib/wineole.rb +10 -1
- data/wineole-bridge-dist/aarch64-pc-windows-gnullvm/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/i686-pc-windows-gnu/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/x86_64-pc-windows-gnu/wineole-bridge.exe +0 -0
- metadata +21 -2
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
require_relative 'errors'
|
|
2
|
+
|
|
3
|
+
module WineOLE
|
|
4
|
+
# COM events for one object, reached as `proxy.ole_events`.
|
|
5
|
+
#
|
|
6
|
+
# Registering a callback is the only thing a caller does. Everything under
|
|
7
|
+
# it is DERIVED from that. The bridge-side subscription and the COM Advise
|
|
8
|
+
# beneath it belong to THIS object: they are put up by its first callback
|
|
9
|
+
# and taken back down by the last one removed from it. The sink on the
|
|
10
|
+
# connection and the dispatcher thread belong to the CONNECTION, so they
|
|
11
|
+
# follow the same rule one level up -- up with the first callback anywhere
|
|
12
|
+
# on the client, down with the last one anywhere on it (see Dispatcher).
|
|
13
|
+
# Making any of them a separate control would allow the state where a
|
|
14
|
+
# callback is registered and the event never arrives, with nothing to show
|
|
15
|
+
# for it.
|
|
16
|
+
#
|
|
17
|
+
# That invariant is a claim about ORDER as much as about bookkeeping, so
|
|
18
|
+
# `on`, `off` and `close` hold @wire_mutex across "decide, then tell the
|
|
19
|
+
# bridge". Deciding under one lock and writing the wire outside it lets two
|
|
20
|
+
# threads reach the bridge in the opposite order to the one they decided
|
|
21
|
+
# in: a subscribe landing after the unsubscribe that was meant to follow it
|
|
22
|
+
# leaves a registered callback whose event can never arrive, and the mirror
|
|
23
|
+
# case leaves the bridge advised with nothing to deliver to. Measured, on
|
|
24
|
+
# the code before this lock existed: wire order subscribe, subscribe,
|
|
25
|
+
# unsubscribe with one callback still registered.
|
|
26
|
+
#
|
|
27
|
+
# Callbacks run on one dispatcher thread PER CONNECTION, in arrival order,
|
|
28
|
+
# one at a time -- so two objects on one client never run their callbacks
|
|
29
|
+
# concurrently, and a caller needs no lock between them. That thread is not
|
|
30
|
+
# here: it belongs to the connection, and this object attaches itself to it
|
|
31
|
+
# for as long as it has a registration (see Dispatcher). They may call COM
|
|
32
|
+
# freely -- that is the point -- because the client's reader thread is a
|
|
33
|
+
# different thread and stays free to read the response. A slow callback
|
|
34
|
+
# delays the events behind it, on every object on the connection.
|
|
35
|
+
#
|
|
36
|
+
# The dispatcher thread survives anything a callback or a malformed frame
|
|
37
|
+
# can throw; see Dispatcher.run.
|
|
38
|
+
class Events
|
|
39
|
+
# NOT a Struct, and not for tidiness: a Struct has value equality, so two
|
|
40
|
+
# subscriptions with the same name, block and flag are `==` without being
|
|
41
|
+
# the same registration -- registering one proc twice for an event is
|
|
42
|
+
# enough. `off(first)` would then remove the second one too (Array#delete
|
|
43
|
+
# removes every match), unsubscribe, and leave the caller with a
|
|
44
|
+
# Subscription that never fires again and nothing said about it. A
|
|
45
|
+
# registration is the thing itself, so identity is its equality.
|
|
46
|
+
#
|
|
47
|
+
# `args` is per callback, but the wire has one flag per event: what goes
|
|
48
|
+
# on it is the union (see `effective_args`).
|
|
49
|
+
class Subscription
|
|
50
|
+
attr_reader :name, :block, :args
|
|
51
|
+
|
|
52
|
+
def initialize(name, block, args)
|
|
53
|
+
@name = name
|
|
54
|
+
@block = block
|
|
55
|
+
@args = args
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(client, handle)
|
|
60
|
+
@client = client
|
|
61
|
+
@handle = handle
|
|
62
|
+
# A plain Hash, deliberately not `Hash.new { |h, k| h[k] = [] }`: with a
|
|
63
|
+
# default block, merely LOOKING an event name up writes a permanent key
|
|
64
|
+
# -- an arriving event nobody subscribed to, or an `off` for a name that
|
|
65
|
+
# was never `on`, would grow this table for the life of the connection.
|
|
66
|
+
# A name is added when a callback is registered for it and removed when
|
|
67
|
+
# the last one goes.
|
|
68
|
+
@subs = {}
|
|
69
|
+
@mutex = Mutex.new
|
|
70
|
+
# Held across the whole of `on`/`off`/`close` -- the decision AND the
|
|
71
|
+
# call that carries it out -- and always acquired BEFORE @mutex, never
|
|
72
|
+
# while holding it. @mutex is therefore never held across a wire round
|
|
73
|
+
# trip, which is what keeps `deliver`'s brief acquisition of it off the
|
|
74
|
+
# bridge's response time. A callback calling `off` from the dispatcher
|
|
75
|
+
# waits for at most one round trip and cannot deadlock: the thread it
|
|
76
|
+
# waits for needs nothing from the dispatcher to finish.
|
|
77
|
+
#
|
|
78
|
+
# The connection's Dispatcher has a mutex of its own, and it is taken
|
|
79
|
+
# under this one and never under @mutex: the order is @wire_mutex ->
|
|
80
|
+
# Dispatcher#@mutex (in `arm`/`disarm`) and @wire_mutex -> @mutex
|
|
81
|
+
# (everywhere else), so those two are never held together. The
|
|
82
|
+
# Dispatcher takes none of these, and drops its own before it calls
|
|
83
|
+
# `deliver`, which is what lets a callback register on another object.
|
|
84
|
+
@wire_mutex = Mutex.new
|
|
85
|
+
@on_error = nil
|
|
86
|
+
# Whether this object currently has a place on the connection's
|
|
87
|
+
# dispatcher. It stands where @sink used to: the dispatcher and its
|
|
88
|
+
# sink are the connection's now, shared with every other object on it,
|
|
89
|
+
# so "is it up?" is no longer a question this object can answer by
|
|
90
|
+
# looking at what it owns. Read and written under @wire_mutex only --
|
|
91
|
+
# `arm` and `disarm` are both called with it held -- which is what
|
|
92
|
+
# keeps `disarm` idempotent, and it has to be: it is reached from
|
|
93
|
+
# `off`'s ensure, from `close`, and from `on`'s rescue, on an object
|
|
94
|
+
# that may never have been armed at all.
|
|
95
|
+
@attached = false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# `args: false` tells the bridge not to mint handles for this event's
|
|
99
|
+
# object arguments. The callback is then called with no arguments at all
|
|
100
|
+
# -- measured: a block written `|sheet, range|` gets nil for both. Worth
|
|
101
|
+
# it for a high-frequency event you only want to count.
|
|
102
|
+
#
|
|
103
|
+
# The bridge holds ONE flag per event, so what goes on the wire is the
|
|
104
|
+
# union: arguments are minted while any callback for that event wants
|
|
105
|
+
# them. Registering an `args: true` callback next to an `args: false` one
|
|
106
|
+
# therefore re-subscribes rather than leaving the first registration's
|
|
107
|
+
# flag standing -- measured on Excel before this was here: the second
|
|
108
|
+
# callback was handed nil, having asked for the objects, and nothing said
|
|
109
|
+
# so. A callback that asked for `args: false` and gets them anyway
|
|
110
|
+
# because a sibling wanted them is the harmless direction of the same
|
|
111
|
+
# trade; it can ignore them.
|
|
112
|
+
def on(name, args: true, &block)
|
|
113
|
+
raise ArgumentError, 'on needs a block -- there is nothing to call otherwise' unless block
|
|
114
|
+
|
|
115
|
+
sub = Subscription.new(name, block, args)
|
|
116
|
+
@wire_mutex.synchronize do
|
|
117
|
+
# Before the subscribe, never after: the bridge advises the COM
|
|
118
|
+
# source as the subscribe is handled, so an event can be on its way
|
|
119
|
+
# back before the call returns. Arming afterwards would drop it.
|
|
120
|
+
arm
|
|
121
|
+
wanted = @mutex.synchronize do
|
|
122
|
+
before = effective_args(name)
|
|
123
|
+
(@subs[name] ||= []) << sub
|
|
124
|
+
after = effective_args(name)
|
|
125
|
+
after == before ? nil : after
|
|
126
|
+
end
|
|
127
|
+
begin
|
|
128
|
+
@client.call('subscribe', {handle: @handle, event: name, args: wanted}) unless wanted.nil?
|
|
129
|
+
rescue StandardError
|
|
130
|
+
# A subscribe the bridge refused -- an object that is not an event
|
|
131
|
+
# source is the ordinary case -- must not leave the callback
|
|
132
|
+
# registered. Keeping it would produce exactly the state this class
|
|
133
|
+
# exists to make unreachable: a callback that is never called, with
|
|
134
|
+
# the error already raised and gone.
|
|
135
|
+
disarm if @mutex.synchronize { drop(name, sub); @subs.empty? }
|
|
136
|
+
raise
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
sub
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Takes either a name (every callback for it) or one Subscription.
|
|
143
|
+
def off(name_or_sub)
|
|
144
|
+
@wire_mutex.synchronize do
|
|
145
|
+
sub = name_or_sub.is_a?(Subscription) ? name_or_sub : nil
|
|
146
|
+
name = sub ? sub.name : name_or_sub.to_s
|
|
147
|
+
before, after, empty = @mutex.synchronize do
|
|
148
|
+
was = effective_args(name)
|
|
149
|
+
drop(name, sub)
|
|
150
|
+
[was, effective_args(name), @subs.empty?]
|
|
151
|
+
end
|
|
152
|
+
# Nothing was registered for this event, so nothing was derived from
|
|
153
|
+
# it either. `after.nil?` alone cannot tell "the last callback just
|
|
154
|
+
# went" from "there was never one", and an unsubscribe for a
|
|
155
|
+
# subscription that does not exist is a round trip that says nothing.
|
|
156
|
+
next if before.nil?
|
|
157
|
+
|
|
158
|
+
begin
|
|
159
|
+
if after.nil?
|
|
160
|
+
# The last callback for this event is gone, so the subscription
|
|
161
|
+
# that only existed to feed it goes too -- and with the last name
|
|
162
|
+
# for the object, the COM Advise underneath it.
|
|
163
|
+
@client.call('unsubscribe', {handle: @handle, event: name})
|
|
164
|
+
elsif after != before
|
|
165
|
+
# Callbacks remain, but the one that wanted arguments was among
|
|
166
|
+
# those removed: stop paying for handles nobody asked for.
|
|
167
|
+
@client.call('subscribe', {handle: @handle, event: name, args: after})
|
|
168
|
+
end
|
|
169
|
+
ensure
|
|
170
|
+
# In an ensure because the registry has already been emptied above:
|
|
171
|
+
# if the unsubscribe raised (a connection that has just gone is the
|
|
172
|
+
# ordinary case) the local half must still come down, or this
|
|
173
|
+
# object keeps a dispatcher thread and a sink entry on the client
|
|
174
|
+
# for the life of the connection with nothing left to deliver.
|
|
175
|
+
disarm if empty
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
self
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Every callback forgotten, every subscription and Advise released, the
|
|
182
|
+
# dispatcher stopped and the sink taken off the connection.
|
|
183
|
+
#
|
|
184
|
+
# `off`-ing the last callback does all of this already -- that is the
|
|
185
|
+
# derivation this class is built on, and it is why there is no `close`
|
|
186
|
+
# you are REQUIRED to call. This is the bulk form, for a caller who does
|
|
187
|
+
# not want to remember which names it registered. `on` afterwards works
|
|
188
|
+
# exactly as the first one did: the object arms itself again.
|
|
189
|
+
def close
|
|
190
|
+
@wire_mutex.synchronize do
|
|
191
|
+
@mutex.synchronize { @subs.keys }.each do |name|
|
|
192
|
+
@mutex.synchronize { drop(name, nil) }
|
|
193
|
+
begin
|
|
194
|
+
@client.call('unsubscribe', {handle: @handle, event: name})
|
|
195
|
+
rescue StandardError
|
|
196
|
+
# A connection that has already gone has already unadvised
|
|
197
|
+
# everything on it. Unlike `off`, this is not reported: `close`
|
|
198
|
+
# is what a caller reaches for when it is done, and the local
|
|
199
|
+
# half it exists to release comes down either way.
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
disarm
|
|
204
|
+
end
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# ONE error handler per object, last writer wins -- deliberately not
|
|
209
|
+
# `on`'s append. An error handler is not a subscription: it has no
|
|
210
|
+
# arguments to negotiate, nothing is derived from it, and there is
|
|
211
|
+
# nothing for a second one to add that the first cannot do. That it
|
|
212
|
+
# returns `self` rather than a Subscription is the same statement, and
|
|
213
|
+
# there is no `off_error` for the same reason -- `on_error { }` replaces
|
|
214
|
+
# it with one that does nothing.
|
|
215
|
+
def on_error(&block)
|
|
216
|
+
@mutex.synchronize { @on_error = block }
|
|
217
|
+
self
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Tests only. Production code never needs any of these, because callbacks
|
|
221
|
+
# are the delivery mechanism.
|
|
222
|
+
#
|
|
223
|
+
# The queue and the thread these two ask about belong to the connection
|
|
224
|
+
# now, so both are the Dispatcher's answers. Kept here as delegators
|
|
225
|
+
# because a test that has an `Events` and asks whether ITS callbacks are
|
|
226
|
+
# still being delivered is asking the right question of the right object
|
|
227
|
+
# -- and because the answer is the same one it always was.
|
|
228
|
+
def stopped_for_test?(seconds)
|
|
229
|
+
@client.dispatcher.stopped_for_test?(seconds)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def drain_for_test(seconds = 5)
|
|
233
|
+
@client.dispatcher.drain_for_test(seconds)
|
|
234
|
+
self
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def registered_names_for_test
|
|
238
|
+
@mutex.synchronize { @subs.keys }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
# Takes this object's place on the connection: the frames for its handle
|
|
244
|
+
# start being routed to it, and the connection's dispatcher thread and
|
|
245
|
+
# sink go up if this is the first object to ask for them. Derived from
|
|
246
|
+
# there being a callback at all, so an `ole_events` a caller merely
|
|
247
|
+
# touched costs nothing anywhere. Called from `on` under @wire_mutex,
|
|
248
|
+
# which is what serializes it against `disarm`.
|
|
249
|
+
def arm
|
|
250
|
+
return if @attached
|
|
251
|
+
|
|
252
|
+
@client.dispatcher.attach(@handle, self)
|
|
253
|
+
@attached = true
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The other half of `arm`, once the last callback is gone: a registered
|
|
257
|
+
# callback is the only reason for any of this to exist. Without it an
|
|
258
|
+
# Events that has had every callback removed still holds a place on the
|
|
259
|
+
# connection's dispatcher and an entry the reader walks for every frame
|
|
260
|
+
# on it -- measured: 50 proxies that registered one callback and removed
|
|
261
|
+
# it left 51 live threads and 50 sink entries, all of them until the
|
|
262
|
+
# connection closed. The thread and the sink themselves only go with the
|
|
263
|
+
# LAST object to leave; that decision belongs to the Dispatcher, which is
|
|
264
|
+
# the only thing that knows whether any other object still has callbacks.
|
|
265
|
+
def disarm
|
|
266
|
+
return unless @attached
|
|
267
|
+
|
|
268
|
+
@attached = false
|
|
269
|
+
# By identity: the Dispatcher removes this object's routing entry and
|
|
270
|
+
# nobody else's.
|
|
271
|
+
@client.dispatcher.detach(@handle, self)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# What the wire flag for `name` should be: nil when no callback is
|
|
275
|
+
# registered for it at all, otherwise true if any of them wants the
|
|
276
|
+
# event's object arguments minted. Call it under @mutex.
|
|
277
|
+
def effective_args(name)
|
|
278
|
+
subs = @subs[name]
|
|
279
|
+
return nil if subs.nil? || subs.empty?
|
|
280
|
+
|
|
281
|
+
subs.any? { |sub| sub.args }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Removes one subscription, or every callback for `name` when `sub` is
|
|
285
|
+
# nil, and takes the name out of the table when nothing is left for it.
|
|
286
|
+
# Call it under @mutex.
|
|
287
|
+
#
|
|
288
|
+
# `equal?`, not `==`: `off` removes the registration it was handed and no
|
|
289
|
+
# other. See Subscription.
|
|
290
|
+
def drop(name, sub)
|
|
291
|
+
list = @subs[name]
|
|
292
|
+
return if list.nil?
|
|
293
|
+
|
|
294
|
+
sub.nil? ? list.clear : list.reject! { |s| s.equal?(sub) }
|
|
295
|
+
@subs.delete(name) if list.empty?
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# One frame, to the callbacks registered for its name. Called by the
|
|
299
|
+
# connection's Dispatcher, which routed it here by handle, and private
|
|
300
|
+
# for the reason the whole class exists: a registered callback is the
|
|
301
|
+
# only way in, and a public `deliver` would say otherwise. The frame's
|
|
302
|
+
# argument handles are given back by the Dispatcher's `route`, in an
|
|
303
|
+
# ensure that covers a frame this raised on as well as one that reached
|
|
304
|
+
# nobody at all.
|
|
305
|
+
def deliver(frame)
|
|
306
|
+
subs = @mutex.synchronize { (@subs[frame['event']] || []).dup }
|
|
307
|
+
args = build_args(frame['args'])
|
|
308
|
+
subs.each do |sub|
|
|
309
|
+
begin
|
|
310
|
+
sub.block.call(*args)
|
|
311
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
312
|
+
# Everything, for the reason Dispatcher.run catches everything: this
|
|
313
|
+
# thread is the whole delivery mechanism, and a callback raising
|
|
314
|
+
# something outside StandardError must not take the next callback,
|
|
315
|
+
# the next event and every later release down with it.
|
|
316
|
+
report(e, frame)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def build_args(raw)
|
|
322
|
+
return [] if raw.nil?
|
|
323
|
+
|
|
324
|
+
# The same decode an invoke's result goes through, so an event argument
|
|
325
|
+
# that is an object arrives as a Proxy and one that is a date arrives
|
|
326
|
+
# as a Time -- a second, private copy of that walk here is how those
|
|
327
|
+
# two answers drift apart.
|
|
328
|
+
raw.map { |v| Proxy.decode(@client, v) }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Never raises. It is called from the dispatcher's own rescue, so an
|
|
332
|
+
# exception out of here is the one thing that could still end the thread.
|
|
333
|
+
def report(err, frame)
|
|
334
|
+
handler = @mutex.synchronize { @on_error }
|
|
335
|
+
event = frame.is_a?(Hash) ? frame['event'] : nil
|
|
336
|
+
begin
|
|
337
|
+
if handler
|
|
338
|
+
handler.call(err, frame)
|
|
339
|
+
else
|
|
340
|
+
warn "wineole: #{event} callback raised #{err.class}: #{err.message}"
|
|
341
|
+
end
|
|
342
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
343
|
+
# An on_error that itself raises must not recurse, and must not be
|
|
344
|
+
# able to kill the dispatcher either -- its own rescue was
|
|
345
|
+
# StandardError once, and an on_error raising past that was a third
|
|
346
|
+
# way to lose every later event in silence.
|
|
347
|
+
warn "wineole: on_error raised #{e.class} while reporting #{err.class}"
|
|
348
|
+
end
|
|
349
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
350
|
+
nil # even $stderr being gone must not end the dispatcher
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module WineOLE
|
|
2
|
+
module MSOffice
|
|
3
|
+
# Parses the addressing DSL this wrapper inherits from msoffice.rb:
|
|
4
|
+
#
|
|
5
|
+
# "[Book1]Sheet1!A1:B2" "[:new]" ":new!" ":first!A1" "A1:B2"
|
|
6
|
+
#
|
|
7
|
+
# Touches no COM: it is a pure string parser, so it can be exercised
|
|
8
|
+
# without Excel running.
|
|
9
|
+
#
|
|
10
|
+
# The patterns come from msoffice.rb, which encodes Excel's actual grid
|
|
11
|
+
# limits (IV/65536 for Excel 11, XFD/1048576 for 12+). Both column
|
|
12
|
+
# patterns were checked against every column up to their limit and are
|
|
13
|
+
# exact -- and they are the harder half.
|
|
14
|
+
#
|
|
15
|
+
# The two row patterns needed a fix, carried here. Every nested digit
|
|
16
|
+
# range started at 1 where it should start at 0: the leading digit's
|
|
17
|
+
# "no leading zero" rule was carried down to positions the digits above
|
|
18
|
+
# had already constrained. That dropped row 65530 outright, and 11,111
|
|
19
|
+
# rows of the 1048576 grid. Both are exhaustively verified now, and
|
|
20
|
+
# test_every_row_in_the_grid_parses is what keeps them that way.
|
|
21
|
+
class Address
|
|
22
|
+
PTN_A_IV = /[A-H]?[A-Z]|I[A-V]/i
|
|
23
|
+
PTN_ABS_A_IV = /\$?#{PTN_A_IV}/
|
|
24
|
+
PTN_1_65536 = /[1-9]\d{,3}|[1-5]\d{4}|6(?:[0-4]\d{3}|5(?:[0-4]\d{2}|5(?:[0-2]\d|3[0-6])))/
|
|
25
|
+
PTN_ABS_1_65536 = /\$?#{PTN_1_65536}/
|
|
26
|
+
PTN_ABS_A1_IV65536 = /#{PTN_ABS_A_IV}#{PTN_ABS_1_65536}/
|
|
27
|
+
PTN_RANGE_LOCAL_XL11 = /(?<range>(?:#{PTN_ABS_A1_IV65536}:)?#{PTN_ABS_A1_IV65536}|#{PTN_ABS_A_IV}:#{PTN_ABS_A_IV}|#{PTN_ABS_1_65536}:#{PTN_ABS_1_65536})/
|
|
28
|
+
PTN_A_XFD = /[A-W]?[A-Z]{1,2}|X(?:[A-E][A-Z]|F[A-D])/i
|
|
29
|
+
PTN_ABS_A_XFD = /\$?#{PTN_A_XFD}/
|
|
30
|
+
PTN_1_1048576 = /[1-9]\d{,5}|10(?:[0-3]\d{4}|4(?:[0-7]\d{3}|8(?:[0-4]\d{2}|5(?:[0-6]\d|7[0-6]))))/
|
|
31
|
+
PTN_ABS_1_1048576 = /\$?#{PTN_1_1048576}/
|
|
32
|
+
PTN_ABS_A1_XFD1048576 = /#{PTN_ABS_A_XFD}#{PTN_ABS_1_1048576}/
|
|
33
|
+
PTN_RANGE_LOCAL_XL12 = /(?<range>(?:#{PTN_ABS_A1_XFD1048576}:)?#{PTN_ABS_A1_XFD1048576}|#{PTN_ABS_A_XFD}:#{PTN_ABS_A_XFD}|#{PTN_ABS_1_1048576}:#{PTN_ABS_1_1048576})/
|
|
34
|
+
PTN_WORKBOOK = /\[(?<workbook>(?i::new)|[^\[\]]*)\]/
|
|
35
|
+
PTN_WORKBOOK_WORKSHEET = /(?<worksheet_quote>'?)#{PTN_WORKBOOK}?(?<worksheet>(?i::(?:new|first|last))|(?:[^\[\]\\\:\*\'][^\[\]\\\:\*]*)?)\k<worksheet_quote>!/
|
|
36
|
+
PTN_XL11 = /\A(?:#{PTN_WORKBOOK}|#{PTN_WORKBOOK_WORKSHEET}?#{PTN_RANGE_LOCAL_XL11}?)\z/
|
|
37
|
+
PTN_XL12 = /\A(?:#{PTN_WORKBOOK}|#{PTN_WORKBOOK_WORKSHEET}?#{PTN_RANGE_LOCAL_XL12}?)\z/
|
|
38
|
+
|
|
39
|
+
attr_reader :workbook, :worksheet, :range
|
|
40
|
+
|
|
41
|
+
# Returns nil when the string is not an address at all, so a caller can
|
|
42
|
+
# fall back to treating it as a raw sheet name -- which is what
|
|
43
|
+
# msoffice.rb did.
|
|
44
|
+
def self.parse(str, excel_version)
|
|
45
|
+
pattern = excel_version.to_f >= 12 ? PTN_XL12 : PTN_XL11
|
|
46
|
+
m = pattern.match(str)
|
|
47
|
+
return nil if m.nil? || m.to_s.empty?
|
|
48
|
+
|
|
49
|
+
new(workbook: m[:workbook], worksheet: m[:worksheet], range: m[:range])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize(workbook:, worksheet:, range:)
|
|
53
|
+
@workbook = workbook
|
|
54
|
+
@worksheet = worksheet
|
|
55
|
+
@range = range
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether this address names a range of cells. An address that stops at
|
|
59
|
+
# a sheet or a book is a lookup, not an assignment target -- see the
|
|
60
|
+
# spec on why `xl["Sheet1!"] = 0` filling a whole sheet is a hazard
|
|
61
|
+
# rather than a convenience.
|
|
62
|
+
def range?
|
|
63
|
+
!@range.nil?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require 'tmpdir'
|
|
2
|
+
require_relative '../proxy'
|
|
3
|
+
require_relative 'passthrough'
|
|
4
|
+
require_relative 'paths'
|
|
5
|
+
require_relative 'sheet'
|
|
6
|
+
require_relative 'vba_api'
|
|
7
|
+
require_relative 'forms'
|
|
8
|
+
|
|
9
|
+
module WineOLE
|
|
10
|
+
module MSOffice
|
|
11
|
+
# Wraps a COM Workbook.
|
|
12
|
+
#
|
|
13
|
+
# Name note (measured against a live Excel 11): `sheet`, `each_sheet`,
|
|
14
|
+
# `save_as` and `local_path` are all free -- COM answers
|
|
15
|
+
# DISP_E_UNKNOWNNAME for each. `close` is not: COM resolves member names
|
|
16
|
+
# case-insensitively, and `Workbook.Close` already exists. It is a
|
|
17
|
+
# deliberate shadow anyway (see #close below). `local_file` is free by
|
|
18
|
+
# the same construction as `save_as`: COM does not strip underscores
|
|
19
|
+
# when matching names, so it never collides with `Workbook.FullName`.
|
|
20
|
+
class Book
|
|
21
|
+
include Passthrough
|
|
22
|
+
|
|
23
|
+
# `convert_paths` is the caller's opt-out; whether converting is even
|
|
24
|
+
# meaningful is a separate question (a remote bridge's own filesystem
|
|
25
|
+
# means nothing to this machine). Combining them once here means a
|
|
26
|
+
# caller cannot talk a remote bridge into converting by passing
|
|
27
|
+
# `convert_paths: true` -- Paths.convertible? still says no.
|
|
28
|
+
def initialize(proxy, client:, version:, convert_paths: true)
|
|
29
|
+
@ole = proxy
|
|
30
|
+
@version = version
|
|
31
|
+
@convert_paths = convert_paths && Paths.convertible?(client: client)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Worksheets, not Sheets: Sheets also includes chart sheets, which
|
|
35
|
+
# this wrapper's Sheet class does not model.
|
|
36
|
+
def sheet(name_or_index)
|
|
37
|
+
Sheet.new(@ole.Worksheets.Item(name_or_index), version: @version)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def each_sheet
|
|
41
|
+
return to_enum(:each_sheet) unless block_given?
|
|
42
|
+
|
|
43
|
+
worksheets = @ole.Worksheets
|
|
44
|
+
(1..worksheets.Count).each do |i|
|
|
45
|
+
yield Sheet.new(worksheets.Item(i), version: @version)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Takes only the path. A caller needing FileFormat and the rest of
|
|
50
|
+
# COM's SaveAs arguments uses the passthrough `book.SaveAs(...)`.
|
|
51
|
+
def save_as(path)
|
|
52
|
+
target = @convert_paths ? Paths.to_wine(path) : path
|
|
53
|
+
@ole.SaveAs(target)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# COM's Workbook.Path is the *containing folder*, not the file --
|
|
57
|
+
# local_path deliberately names the same thing this wrapper's way, in
|
|
58
|
+
# Linux form. The file's own path is #local_file.
|
|
59
|
+
#
|
|
60
|
+
# An unsaved book's Path is "" -- Paths.to_local returns that
|
|
61
|
+
# unchanged without shelling out to winepath, so this never runs it
|
|
62
|
+
# for nothing.
|
|
63
|
+
def local_path
|
|
64
|
+
@convert_paths ? Paths.to_local(@ole.Path) : @ole.Path
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The file's own path, in Linux form -- what local_path is not.
|
|
68
|
+
# Gated by the same @convert_paths (loopback-only) rule as
|
|
69
|
+
# local_path; calling Paths.to_local(book.FullName) directly instead
|
|
70
|
+
# skips that gate and runs a local winepath over what may be a
|
|
71
|
+
# *remote* bridge's Wine path, silently producing a path that refers
|
|
72
|
+
# to a filesystem this machine does not have (Spec Sec 4.7: a wrong
|
|
73
|
+
# conversion that happens silently is worse than one that visibly
|
|
74
|
+
# does not happen).
|
|
75
|
+
#
|
|
76
|
+
# Measured against a live Excel 11: FullName is the file
|
|
77
|
+
# (`Z:\tmp\wineole_item_probe.xls` where Path is the folder,
|
|
78
|
+
# `Z:\tmp`); an unsaved book's FullName is the bare in-memory name
|
|
79
|
+
# ("Book1"), matching its Path of "".
|
|
80
|
+
def local_file
|
|
81
|
+
@convert_paths ? Paths.to_local(@ole.FullName) : @ole.FullName
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A deliberate shadow of COM's Workbook.Close. Close with no
|
|
85
|
+
# SaveChanges argument can raise a modal save-changes prompt, which
|
|
86
|
+
# under Wine is a hang; close(save: false) turns that hazard into an
|
|
87
|
+
# explicit parameter instead. The raw member stays reachable as
|
|
88
|
+
# `book.Close(...)` (exact PascalCase) and as `book.ole.Close(...)`.
|
|
89
|
+
def close(save: false)
|
|
90
|
+
@ole.Close(save)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# This workbook's VBA surface: blocks, components, import and export.
|
|
94
|
+
# `book.vba.write(code, name: 'helpers')` is the common call; see
|
|
95
|
+
# BookVBA for the block-vs-component split and for where code has to
|
|
96
|
+
# live to be callable at all.
|
|
97
|
+
#
|
|
98
|
+
# Name note: `vba` is a bare lowercase word, so it would shadow a COM
|
|
99
|
+
# member spelled the same. Workbook has none -- the member it has is
|
|
100
|
+
# `VBProject`, still reachable as `book.vba.project`.
|
|
101
|
+
def vba
|
|
102
|
+
@vba ||= BookVBA.new(@ole, convert_paths: @convert_paths)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# This workbook's UserForms: `forms.add('AppForm')`, `forms['AppForm']`.
|
|
106
|
+
#
|
|
107
|
+
# Name note (M0, measured against Excel 11): `forms` is free on
|
|
108
|
+
# Workbook.
|
|
109
|
+
def forms
|
|
110
|
+
@forms ||= Forms.new(self)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module WineOLE
|
|
2
|
+
module MSOffice
|
|
3
|
+
# '#RRGGBB' <-> Excel's colour integer.
|
|
4
|
+
#
|
|
5
|
+
# Excel stores a colour as BGR, not RGB: measured by making Excel name
|
|
6
|
+
# the colour itself, `Interior.Color = 255` reports ColorIndex 3 (red)
|
|
7
|
+
# and `= 0xFF0000` reports ColorIndex 5 (blue). So the obvious
|
|
8
|
+
# `'#FF0000'.delete('#').to_i(16)` produces blue, silently.
|
|
9
|
+
#
|
|
10
|
+
# Public, and returning a plain Integer, on purpose. The wrapper's own
|
|
11
|
+
# keys are not the only place a colour is written -- the passthrough
|
|
12
|
+
# reaches Interior.Color, Font.Color, Borders.Color, Tab.Color and
|
|
13
|
+
# anything else COM has, and that surface is unbounded by design. One
|
|
14
|
+
# function that returns an Integer covers all of it without the protocol
|
|
15
|
+
# or Proxy#encode knowing anything about colours.
|
|
16
|
+
module Color
|
|
17
|
+
HEX = /\A#?(?:\h{3}|\h{6})\z/.freeze
|
|
18
|
+
private_constant :HEX
|
|
19
|
+
|
|
20
|
+
# '#RRGGBB' | '#RGB' | [r, g, b] -> Excel's integer.
|
|
21
|
+
def self.[](value)
|
|
22
|
+
r, g, b = rgb(value)
|
|
23
|
+
r | (g << 8) | (b << 16)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Excel's integer (or the Float it actually hands back) -> '#RRGGBB'.
|
|
27
|
+
def self.to_hex(value)
|
|
28
|
+
unless value.is_a?(::Numeric)
|
|
29
|
+
raise ArgumentError, "expected a number from COM, got #{value.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Checked before converting: Float::INFINITY#to_i raises
|
|
33
|
+
# FloatDomainError and Complex#to_i raises RangeError, neither of
|
|
34
|
+
# which is this module's ArgumentError. Range#cover? (as
|
|
35
|
+
# Format.size uses via SIZE_RANGE) rather than #between?, because
|
|
36
|
+
# Complex does not include Comparable -- #between? raises
|
|
37
|
+
# NoMethodError on it outright, where #cover? just answers false.
|
|
38
|
+
unless (0..0xFFFFFF).cover?(value)
|
|
39
|
+
raise ArgumentError,
|
|
40
|
+
"expected a colour in 0..0xFFFFFF, got #{value.inspect}. " \
|
|
41
|
+
"Excel's ColorIndex is a different property from Color -- its " \
|
|
42
|
+
'values (-4105 automatic, -4142 none) are not colours and cannot ' \
|
|
43
|
+
'be converted here'
|
|
44
|
+
end
|
|
45
|
+
n = value.to_i
|
|
46
|
+
::Kernel.format('#%02X%02X%02X', n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.rgb(value)
|
|
50
|
+
case value
|
|
51
|
+
when ::String then from_hex(value)
|
|
52
|
+
when ::Array then from_array(value)
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError,
|
|
55
|
+
"expected '#RRGGBB', '#RGB' or [r, g, b], got #{value.inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
private_class_method :rgb
|
|
59
|
+
|
|
60
|
+
def self.from_hex(value)
|
|
61
|
+
# Checked before parsing: String#to_i(16) reads garbage as 0 rather
|
|
62
|
+
# than complaining, so '#GGGGGG' would silently become black.
|
|
63
|
+
unless value.match?(HEX)
|
|
64
|
+
raise ArgumentError,
|
|
65
|
+
"expected '#RRGGBB' or '#RGB', got #{value.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
s = value.delete_prefix('#')
|
|
69
|
+
s = s.chars.map { |c| c * 2 }.join if s.length == 3
|
|
70
|
+
[s[0, 2], s[2, 2], s[4, 2]].map { |h| h.to_i(16) }
|
|
71
|
+
end
|
|
72
|
+
private_class_method :from_hex
|
|
73
|
+
|
|
74
|
+
def self.from_array(value)
|
|
75
|
+
ok = value.length == 3 &&
|
|
76
|
+
value.all? { |c| c.is_a?(::Integer) && (0..255).cover?(c) }
|
|
77
|
+
unless ok
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"expected [r, g, b] with three integers in 0..255, got #{value.inspect}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
value
|
|
83
|
+
end
|
|
84
|
+
private_class_method :from_array
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|