icalPal 3.9.3 → 3.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4853d1f050b001c63c1890618f5393eaee86c812f86cacf8be8b9da483fc67d1
4
- data.tar.gz: ae170e19546fc153bdfbcd6cd89e02ac91c694e2c393e6624e31d437a5b03a34
3
+ metadata.gz: a19cb126e559cf9684d82d4d47eebfefe1a058dadc63ca642693d1ac488a97b0
4
+ data.tar.gz: 562f98a62d93eda9a03de31a5d1a09a7ddf9841d97abe0ea7d335a0976625dc7
5
5
  SHA512:
6
- metadata.gz: 3bf5728beb2504a19f2fae851413c26c6ee192fe2f09e066f8f9b1887e3eb16102eb9a89a4eb5e717e727564e8ca4a74bd09dc3d2743974cf2d33a24d7b25b5d
7
- data.tar.gz: b60b90e37a562ca7df73c53e0e6ade623baa857b3f520a7faded502e7d588c7cbcb3ef2ee02bc9f486bd840f11b4e7638d92f769570e6b216e3bff9b2eaae5af
6
+ metadata.gz: 659550c0e455ff95a565dbc7d2af162872e14126b35f50f7c8cbaee43c154252d2ef45748421540ab2bc7bde89e50aba0135280a093d086a37e3df970adefd89
7
+ data.tar.gz: 941c218eef0998cc5356dac3e23cd91ea00046f6463b01e0bf6ebb651f008ce00c615d3c787945ec707a9fda35cdb25e733f10058464234ccb93d092213d727e
data/README.md CHANGED
@@ -1,15 +1,18 @@
1
- [![Gem Version](https://badge.fury.io/rb/icalPal.svg)](https://badge.fury.io/rb/icalPal)
1
+ ![GitHub Release](https://img.shields.io/github/v/release/ajrosen/icalPal?display_name=tag&logo=rubygems&label=gem%20version)
2
+ [![Downloads](https://img.shields.io/gem/dtv/icalPal?label=downloads)](https://badge.fury.io/rb/icalPal.svg?icon=si%3Arubygems&icon_color=%23ff0000)
3
+ [![Downloads](https://img.shields.io/gem/dt/icalPal?label=total%20downloads)](https://badge.fury.io/rb/icalPal.svg?icon=si%3Arubygems&icon_color=%23ff0000)
2
4
 
3
5
  # icalPal
4
6
 
5
7
  ## Description
6
8
 
7
- icalPal is a command-line tool to query macOS Calendar and Reminders
8
- databases for accounts, calendars, events, and tasks. It can be run
9
- on any system with [Ruby](https://www.ruby-lang.org/) and access to a
10
- Calendar or Reminders database.
9
+ icalPal is a command-line tool to query macOS *Calendar* and
10
+ *Reminders* databases for accounts, calendars, events, and tasks. It
11
+ can be run on any system with [Ruby](https://www.ruby-lang.org/) and
12
+ access to a Calendar or Reminders database.
11
13
 
12
14
  <!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
15
+
13
16
  **Table of Contents**
14
17
 
15
18
  - [Installation](#installation)
@@ -26,13 +29,13 @@ Calendar or Reminders database.
26
29
 
27
30
  ## Installation
28
31
 
29
- As a system-wide Ruby gem:
32
+ Install icalPal for all users:
30
33
 
31
34
  ```
32
35
  gem install icalPal
33
36
  ```
34
37
 
35
- In your home directory:
38
+ For only you:
36
39
 
37
40
  ```
38
41
  gem install --user-install icalPal
@@ -64,6 +67,8 @@ differences to be aware of.
64
67
 
65
68
  ### Additional commands
66
69
 
70
+ icalPal also supports additional commands.
71
+
67
72
  ```icalPal accounts```
68
73
 
69
74
  Shows a list of enabled Calendar accounts. Internally they are known
@@ -79,7 +84,7 @@ Shows only reminders that have a due date.
79
84
  *remindersDueBefore* can be used instead of *tasks*
80
85
 
81
86
  Reminders can also be viewed in the *Scheduled Reminders* calendar,
82
- using the *tasks* commands. Repeating reminders are treated the same
87
+ using the *events* commands. Repeating reminders are treated the same
83
88
  as repeating events.
84
89
 
85
90
  ### Additional options
@@ -98,39 +103,35 @@ as repeating events.
98
103
  * ```--color``` uses a wider color palette. Colors are what you have chosen in the Calendar and Reminders apps, including custom colors
99
104
  * ```--match``` lets you filter the results of any command to items where a *FIELD* matches a regular expression. Eg., ```--match notes=zoom.us``` to show only Zoom meeetings
100
105
 
101
- Because icalPal is written in Ruby, and not a native Mac application,
102
- you can run it just about anywhere. It's been tested with the
103
- versions of Ruby included with macOS Sequoia and Tahoe (2.6.10) and
104
- [Homebrew](https://brew.sh/) (3.4.x).
105
-
106
106
  ### Additional properties
107
107
 
108
108
  Several additional properties are available for each command.
109
109
 
110
110
  * Accounts
111
111
  * account
112
+ * delegations
112
113
  * notes
113
114
  * owner
114
115
  * type
115
- * delegations
116
116
 
117
117
  * Calendar
118
118
  * account
119
- * shared\_owner_name, shared\_owner_address
119
+ * locale
120
+ * notes
121
+ * published_URL
120
122
  * self\_identity_email, owner\_identity_email
123
+ * shared\_owner_name, shared\_owner_address
124
+ * sharees
121
125
  * subcal_account_id, subcal_url
122
- * published_URL
123
- * notes
124
- * locale
125
126
 
126
127
  * Tasks
127
- * id
128
- * grocery
128
+ * assignee
129
129
  * completed
130
+ * grocery
130
131
  * group
132
+ * id
131
133
  * section
132
134
  * tags
133
- * assignee
134
135
  * timezone
135
136
  * Notifications
136
137
  * due (due_date formatted with --df and --tf options)
@@ -143,7 +144,7 @@ Several additional properties are available for each command.
143
144
  icalPal: Usage: icalPal [options] [-c] COMMAND
144
145
 
145
146
  COMMAND must be one of the following:
146
- ```
147
+
147
148
  events Print events
148
149
  tasks Print tasks
149
150
  calendars Print calendars
@@ -159,29 +160,27 @@ COMMAND must be one of the following:
159
160
 
160
161
  stores can be used instead of accounts
161
162
  reminders can be used instead of tasks
162
- ```
163
163
 
164
164
  Global options:
165
- ```
165
+
166
166
  -c, --cmd=COMMAND Command to run
167
167
  --db=DB Use DB file instead of Calendar
168
- (default: ["$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb", $HOME/Library/Calendars/Calendar.sqlitedb]
168
+ (default: ["$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb", "$HOME/Library/Calendars/Calendar.sqlitedb"]
169
169
  For the tasks commands this should be a directory containing .sqlite files
170
- (default: "$HOME/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores")
170
+ (default: ["$HOME/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores"])
171
171
  --cf=FILE Set config file path (default: $HOME/.icalpal)
172
172
  --norc Ignore ICALPAL and ICALPAL_CONFIG environment variables
173
173
  -o, --output=FORMAT Print as FORMAT (default: default)
174
174
  [ansi, csv, default, hash, html, json, md, rdoc, remind, toc, xml, yaml]
175
- ```
176
175
 
177
- Including/excluding accounts, calendars, reminders and items:
178
- ```
176
+ Including/excluding accounts, calendars, items:
177
+
179
178
  --is=ACCOUNTS List of accounts to include
180
179
  --es=ACCOUNTS List of accounts to exclude
181
180
 
182
181
  --it=TYPES List of calendar types to include
183
182
  --et=TYPES List of calendar types to exclude
184
- [Local, Exchange, CalDAV, MobileMe, Subscribed, Birthdays]
183
+ [Local, Exchange, CalDAV, MobileMe, Subscribed, Birthdays, Reminders]
185
184
 
186
185
  --ic=CALENDARS List of calendars to include
187
186
  --ec=CALENDARS List of calendars to exclude
@@ -192,11 +191,10 @@ Including/excluding accounts, calendars, reminders and items:
192
191
  --id Include completed reminders
193
192
  --ed Exclude uncompleted reminders
194
193
 
195
- --match=FIELD=REGEX Include only items whose FIELD matches REGEXP (ignoring case)
196
- ```
194
+ --match=FIELD=REGEX Include only items whose FIELD matches REGEX (ignoring case)
197
195
 
198
196
  Choosing dates:
199
- ```
197
+
200
198
  --from=DATE List events starting on or after DATE
201
199
  --to=DATE List events starting on or before DATE
202
200
  DATE can be yesterday, today, tomorrow, +N, -N, or anything accepted by DateTime.parse()
@@ -207,10 +205,9 @@ Choosing dates:
207
205
  --sed Show empty dates with --sd
208
206
  --ia Include only all-day events
209
207
  --ea Exclude all-day events
210
- ```
211
208
 
212
209
  Choose properties to include in the output:
213
- ```
210
+
214
211
  --iep=PROPERTIES List of properties to include
215
212
  --eep=PROPERTIES List of properties to exclude
216
213
  --aep=PROPERTIES List of properties to include in addition to the default list
@@ -231,10 +228,9 @@ Choose properties to include in the output:
231
228
 
232
229
  Use 'all' for PROPERTIES to include all available properties (except any listed in --eep)
233
230
  Use 'list' for PROPERTIES to list all available properties and exit
234
- ```
235
231
 
236
232
  Formatting the output:
237
- ```
233
+
238
234
  --li=N Show at most N items (default: 0 for no limit)
239
235
 
240
236
  --sc Separate by calendar
@@ -261,24 +257,21 @@ Formatting the output:
261
257
 
262
258
  -f Format output using standard ANSI colors
263
259
  --color Format output using a larger color palette
264
- ```
265
260
 
266
261
  Help:
267
- ```
262
+
268
263
  -h, --help Show this message
269
- -V, -v, --version Show version and exit (3.9.1)
264
+ -V, -v, --version Show version and exit (3.10.0)
270
265
  -d, --debug=LEVEL Set the logging level (default: warn)
271
266
  [debug, info, warn, error, fatal]
272
- ```
273
267
 
274
268
  Environment variables:
275
- ```
269
+
276
270
  ICALPAL Additional arguments
277
271
  ICALPAL_CONFIG Additional arguments from a file
278
272
  (default: $HOME/.icalpal)
279
273
 
280
274
  Do not quote or escape values. Options set in ICALPAL override ICALPAL_CONFIG. Options on the command line override ICALPAL.
281
- ```
282
275
 
283
276
  ## Output formats
284
277
 
@@ -295,35 +288,9 @@ properly formatted.
295
288
 
296
289
  Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
297
290
  [RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
298
- framework to build and render the items.
299
-
300
- Each item to be printed is a new
301
- [RDoc::Markup::Document](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Document.html).
302
-
303
- When using one of the _separate by_ options, a section header is added
304
- first. The section contains:
305
-
306
- * [RDoc::Markup::BlankLine](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/BlankLine.html)
307
- (unless this is the first section)
308
- * RDoc::Markup::Heading (level 1)
309
- * [RDoc::Markup::Rule](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Rule.html)
310
-
311
- The rest of the document is a series of
312
- [RDoc::Markup::List](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/List.html)
313
- objects, one for each of the item's properties:
314
-
315
- * [RDoc::Markup::List](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/List.html)
316
- * RDoc::Markup::Heading (level 2)
317
- * [RDoc::Markup::BlankLine](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/BlankLine.html)
318
- * [RDoc::Markup::ListItem](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/ListItem.html)
319
- * [RDoc::Markup::Paragraph](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Paragraph.html)
320
-
321
- The document will also include a number of
322
- [RDoc::Markup::Verbatim](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Verbatim.html)
323
- and
324
- [RDoc::Markup::Raw](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Raw.html)
325
- items. They are not included in the output, but are used to pass
326
- information about the item and property to the default formatter.
291
+ framework to build and render the items. See
292
+ [RDoc](https://github.com/ajrosen/icalPal/blob/main/RDoc.md) for a
293
+ breakdown of how icalPal uses RDoc::Markup.
327
294
 
328
295
  ## History
329
296
 
@@ -341,9 +308,14 @@ Lawton](https://github.com/jimlawton) that it even compiles anymore.
341
308
  Instead of trying to understand and extend the existing code, I chose
342
309
  to start anew using my language of choice:
343
310
  [Ruby](https://www.ruby-lang.org). Using Ruby meant there is *much*
344
- less code; a little over 2,000 lines vs. 7,000. It also means icalPal
311
+ less code; a little over 2,200 lines vs. 7,000. It also means icalPal
345
312
  is multi-platform.
346
313
 
314
+ Because icalPal is written in Ruby, and not a native Mac application,
315
+ you can run it just about anywhere. It's been tested with the
316
+ versions of Ruby included with macOS Sequoia and Tahoe (2.6.10) and
317
+ [Homebrew](https://brew.sh/) (3.4.x).
318
+
347
319
  I won't pretend to understand **why** you would want to run this on
348
320
  Linux or Windows. But since icalPal is written in Ruby and gets its
349
321
  data directly from the Calendar and Reminders database files instead
data/bin/icalPal CHANGED
@@ -206,10 +206,10 @@ $rows.each do |row|
206
206
  next if $opts[:ed] && item['completed'].zero?
207
207
  next unless $opts[:id] || item['completed'].zero?
208
208
 
209
- next if $opts[:dated] == 1 && item['due_date'] && item['due_date'].positive?
210
- next if $opts[:dated] == 2 && (!item['due_date'] || item['due_date'].zero?)
209
+ next if $opts[:dated] == 'undatedTasks' && item['due_date'] && item['due_date'].positive?
210
+ next if $opts[:dated] == 'datedTasks' && (!item['due_date'] || item['due_date'].zero?)
211
211
 
212
- if $opts[:dated] == 3
212
+ if $opts[:dated] == 'tasksDueBefore'
213
213
  next unless item['due_date']
214
214
  next unless item['due_date'].between?($opts[:from].to_i, $opts[:to].to_i)
215
215
  end
@@ -231,14 +231,14 @@ end
231
231
 
232
232
  # Sort the rows
233
233
  begin
234
- sort = []
235
- sort.push $opts[:sep] if $opts[:sep]
236
- sort.push $opts[:sort] if $opts[:sort]
237
- sort.push 'sdate'
234
+ $sort_attrs = []
235
+ $sort_attrs.push $opts[:sep] if $opts[:sep]
236
+ $sort_attrs.push $opts[:sort] if $opts[:sort]
237
+ $sort_attrs.push 'sdate'
238
238
 
239
- $log.info("Sorting #{$items.count} items by #{sort}, reverse #{$opts[:reverse].inspect}")
239
+ $log.info("Sorting #{$items.count} items by #{$sort_attrs}, reverse #{$opts[:reverse].inspect}")
240
240
 
241
- $items.sort_by! { |i| [ i[sort[0]], i[sort[1]], i[sort[2]] ] }
241
+ $items.sort!
242
242
  $items.reverse! if $opts[:reverse]
243
243
  rescue Exception => e
244
244
  $log.info("Sorting failed: #{e}\n")
@@ -321,8 +321,10 @@ items.each_with_index do |i, j|
321
321
  doc << RDoc::Markup::Raw.new($opts[:sep])
322
322
 
323
323
  doc << RDoc::Markup::BlankLine.new if j.positive?
324
- doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
325
- doc << RDoc::Markup::Rule.new(0)
324
+ if i[$opts[:sep]]
325
+ doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
326
+ doc << RDoc::Markup::Rule.new(0)
327
+ end
326
328
 
327
329
  section = i[$opts[:sep]]
328
330
  end
data/ext/extconf.rb CHANGED
@@ -7,7 +7,7 @@ begin
7
7
  Gem.path.each { |p| gemdir = p if File.writable? p }
8
8
 
9
9
  # Dependencies common to all environments
10
- dependencies = %w[ plist tzinfo ]
10
+ dependencies = %w[ plist ]
11
11
 
12
12
  if RUBY_VERSION >= '3.4'
13
13
  # bigdecimal is not part of the default gems starting from Ruby 3.4.0.
data/lib/ToICalPal.rb CHANGED
@@ -97,7 +97,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
97
97
 
98
98
  # Add a blank line
99
99
  #
100
- # @param *_arg [Array] Ignored
100
+ # @param _arg [Array] Ignored
101
101
  def accept_blank_line(*_arg)
102
102
  @res << "\n"
103
103
  end
data/lib/calendar.rb CHANGED
@@ -12,6 +12,12 @@ module ICalPal
12
12
  end
13
13
  end
14
14
 
15
+ def initialize(obj)
16
+ super
17
+
18
+ @self['sharees'] = JSON.parse(obj['sharees'])
19
+ end
20
+
15
21
  QUERY = <<~SQL.freeze
16
22
  SELECT DISTINCT
17
23
 
@@ -23,6 +29,8 @@ c1.title AS calendar,
23
29
  c1.shared_owner_name,
24
30
  c1.shared_owner_address,
25
31
 
32
+ json_group_array(i1.display_name) AS sharees,
33
+
26
34
  c1.published_URL,
27
35
  c1.self_identity_email,
28
36
  c1.owner_identity_email,
@@ -34,10 +42,14 @@ c1.locale
34
42
  FROM #{self.name.split('::').last} c1
35
43
 
36
44
  JOIN Store s1 ON c1.store_id = s1.rowid
45
+ LEFT OUTER JOIN Sharee s2 ON c1.rowid = s2.owner_id
46
+ LEFT OUTER JOIN Identity i1 ON s2.identity_id = i1.rowid
37
47
 
38
48
  WHERE s1.disabled IS NOT 1
39
49
  AND s1.display_order IS NOT -1
40
50
  AND c1.flags IS NOT 519
51
+
52
+ GROUP BY c1.title
41
53
  SQL
42
54
 
43
55
  end
data/lib/defaults.rb CHANGED
@@ -49,31 +49,6 @@ $defaults = {
49
49
  sort: 'prio',
50
50
  },
51
51
 
52
- undatedTasks: {
53
- dated: 1,
54
- db: [ ICalPal::Reminder::DB_PATH ],
55
- iep: %w[ title notes due priority ],
56
- ps: [ "\n " ],
57
- sort: 'prio',
58
- },
59
-
60
- datedTasks: {
61
- dated: 2,
62
- db: [ ICalPal::Reminder::DB_PATH ],
63
- iep: %w[ title notes due priority ],
64
- ps: [ "\n " ],
65
- sort: 'prio',
66
- },
67
-
68
- tasksDueBefore: {
69
- dated: 3,
70
- db: [ ICalPal::Reminder::DB_PATH ],
71
- iep: %w[ title notes due priority ],
72
- ps: [ "\n " ],
73
- sort: 'prio',
74
- to: $today,
75
- },
76
-
77
52
  accounts: {
78
53
  iep: %w[ account type ],
79
54
  sort: 'account',
data/lib/event.rb CHANGED
@@ -16,14 +16,14 @@ module ICalPal
16
16
  end
17
17
 
18
18
  # Standard accessor with special handling for +age+,
19
- # +availability+, +datetime+, +location+, +notes+, +status+,
20
- # +title+, and +uid+
19
+ # +availability+, +datetime+, +location+, +notes+, +sday+,
20
+ # +status+, +uid+, and +event+/+name+/+title+
21
21
  #
22
22
  # @param k [String] Key/property name
23
23
  def [](k)
24
24
  case k
25
25
  when 'age' # pseudo-property
26
- @self['sdate'].year - @self['edate'].year
26
+ @self['sdate'].year - Time.at(@self['start_date'] + ITIME).year
27
27
 
28
28
  when 'availability' # Integer -> String
29
29
  EventKit::EKEventAvailability.select { |_k, v| v == @self['availability'] }.keys
@@ -48,7 +48,7 @@ module ICalPal
48
48
  (@self['notes'])? @self['notes'].strip.gsub("\n", $opts[:nnr]) : nil
49
49
 
50
50
  when 'sday' # pseudo-property
51
- @self['sdate'].day_start
51
+ @self['sdate'].day_start(0)
52
52
 
53
53
  when 'status' # Integer -> String
54
54
  EventKit::EKEventStatus.select { |_k, v| v == @self['status'] }.keys[0]
@@ -76,7 +76,7 @@ module ICalPal
76
76
  'sdate' => obj,
77
77
  'placeholder' => true,
78
78
  'title' => 'Nothing.',
79
- } if obj.is_a?(DateTime) && $opts[:sed]
79
+ } if $opts[:sed] && obj.is_a?(DateTime)
80
80
 
81
81
  super
82
82
 
@@ -90,10 +90,13 @@ module ICalPal
90
90
  obj.keys.select { |i| i.end_with? '_date' }.each do |k|
91
91
  next unless obj[k]
92
92
 
93
+ zone = nil
94
+ zone = '+00:00' if obj['all_day'].positive?
95
+
93
96
  # Save as seconds, Time, RDT
94
97
  ctime = obj[k] + ITIME
95
- ctime -= $now.utc_offset if obj['start_tz'] == '_float'
96
- ttime = Time.at(ctime)
98
+ ctime -= Time.at(ctime).utc_offset if obj["#{k.split('_')[0]}_tz"] == '_float' && !zone
99
+ ttime = Time.at(ctime, in: zone)
97
100
 
98
101
  @self["#{k[0]}seconds"] = ctime
99
102
  @self["#{k[0]}ctime"] = ttime
@@ -173,7 +176,7 @@ module ICalPal
173
176
  skip = true if codate == odate
174
177
  end
175
178
 
176
- events.push(clone(occurrence)) if in_window?(occurrence['sdate'], occurrence['edate']) && !skip
179
+ events.push(clone(occurrence)) if !skip && in_window?(occurrence['sdate'], occurrence['edate'])
177
180
  end
178
181
 
179
182
  # Handle frequency and interval
@@ -261,11 +264,16 @@ module ICalPal
261
264
  c['sdate'] = ICalPal.nth(nth, day, nsdate)
262
265
  c['edate'] = ICalPal.nth(nth, day, nedate)
263
266
  else
264
- diff = day - c['sdate'].wday
265
- diff += 7 if diff.negative?
267
+ %w[ sdate edate ].each do |d|
268
+ diff = day - c[d].wday
269
+ diff += 7 if diff.negative?
270
+
271
+ t1 = Time.at(c[d].to_time)
272
+ t2 = Time.at(t1.to_i + (diff * 86_400))
273
+ t2 += (t1.gmt_offset - t2.gmt_offset)
266
274
 
267
- c['sdate'] += diff
268
- c['edate'] += diff
275
+ c[d] = RDT.from_time(t2)
276
+ end
269
277
  end
270
278
 
271
279
  o.push(clone(c)) if in_window?(c['sdate'], c['edate'])
@@ -277,18 +285,18 @@ module ICalPal
277
285
 
278
286
  # Apply frequency and interval
279
287
  def apply_frequency!
280
- # Leave edate alone for birthdays to compute age
281
- dates = [ 'sdate' ]
282
- dates << 'edate' unless self['calendar'].include?('Birthday')
283
-
284
- dates.each do |d|
288
+ %w[ sdate edate ].each do |d|
285
289
  case EventKit::EKRecurrenceFrequency[self['frequency'] - 1]
286
- when 'daily' then self[d] += self['interval']
287
- when 'weekly' then self[d] += self['interval'] * 7
288
- when 'monthly' then self[d] >>= self['interval']
289
- when 'yearly' then self[d] >>= self['interval'] * 12
290
+ when 'daily' then nd = self[d] + self['interval']
291
+ when 'weekly' then nd = self[d] + (self['interval'] * 7)
292
+ when 'monthly' then nd = self[d] >> self['interval']
293
+ when 'yearly' then nd = self[d] >> (self['interval'] * 12)
290
294
  else $log.error("Unknown frequency: #{self['frequency']}")
291
295
  end
296
+
297
+ # Create a new Time object in case we crossed a daylight saving change
298
+ t = Time.parse("#{nd.year}-#{nd.month}-#{nd.day} #{nd.hour}:#{nd.min}:#{nd.sec}")
299
+ self[d] = RDT.from_time(t)
292
300
  end
293
301
  end
294
302
 
@@ -307,8 +315,8 @@ module ICalPal
307
315
  $log.debug("not now: #{s} to #{e} vs. #{$now}")
308
316
  false
309
317
  end
310
- elsif ([ s, e ].max >= $opts[:from] && s < $opts[:to])
311
- $log.debug("in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
318
+ elsif (s < $opts[:to] && [ s, e ].max >= $opts[:from])
319
+ $log.debug("#{@self['title']} in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
312
320
  true
313
321
  else
314
322
  $log.debug("not in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
@@ -335,6 +343,7 @@ CalendarItem.all_day,
335
343
  CalendarItem.availability,
336
344
  CalendarItem.conference_url_detected,
337
345
  CalendarItem.description AS notes,
346
+ CalendarItem.end_tz,
338
347
  CalendarItem.has_recurrences,
339
348
  CalendarItem.invitation_status,
340
349
  CalendarItem.orig_item_id,
data/lib/icalPal.rb CHANGED
@@ -149,6 +149,25 @@ module ICalPal
149
149
  @self.values
150
150
  end
151
151
 
152
+ # @see Array.<=>
153
+ #
154
+ # If either self or other is nil, but not both, the nil object is
155
+ # always less than
156
+ def <=>(other)
157
+ $sort_attrs.each do |s|
158
+ next if self[s] == other[s]
159
+
160
+ # nil is always less than
161
+ return -1 if other[s].nil?
162
+ return 1 if self[s].nil?
163
+
164
+ return -1 if self[s] < other[s]
165
+ return 1 if self[s] > other[s]
166
+ end
167
+
168
+ 0
169
+ end
170
+
152
171
  # Like inspect, but easier for humans to read
153
172
  #
154
173
  # @return [Array<String>] @self as a key=value array, sorted by key
data/lib/options.rb CHANGED
@@ -268,17 +268,23 @@ module ICalPal
268
268
 
269
269
  when 'Remaining'
270
270
  cli[:from] = RDT.from_time($now)
271
- cli[:to] = $today.day_end
271
+ cli[:to] = $today.day_end(0)
272
272
  cli[:days] = 1
273
273
  end
274
274
  end
275
275
 
276
276
  # Handle tasks command variants
277
- if cli[:cmd] == 'tasksDueBefore'
278
- cli.delete(:days) unless cli[:days]
279
- cli[:from] = RDT.from_epoch(0) unless cli[:from]
277
+ if cli[:cmd] =~ /tasks/i
278
+ cli[:dated] = cli[:cmd]
279
+
280
+ if cli[:dated] == 'tasksDueBefore'
281
+ cli.delete(:days) unless cli[:days]
282
+ cli[:from] = RDT.from_epoch(0) unless cli[:from]
283
+ cli[:to] = $today unless cli[:to]
284
+ end
285
+
286
+ cli[:cmd] = 'tasks'
280
287
  end
281
- cli[:cmd] = 'tasks' if %w[ datedTasks undatedTasks tasksDueBefore ].include? cli[:cmd]
282
288
 
283
289
  # Must have a valid command
284
290
  raise(OptionParser::InvalidArgument, "Unknown COMMAND #{cli[:cmd]}") unless (COMMANDS.any? cli[:cmd])
data/lib/rdt.rb CHANGED
@@ -25,15 +25,25 @@ module ICalPal
25
25
  # @return [RDT] a new RDT
26
26
  def self.conv(str)
27
27
  case str
28
- when 'yesterday' then $today - 1
28
+ when 'yesterday' then $today.add(-1)
29
29
  when 'today' then $today
30
- when 'tomorrow' then $today + 1
30
+ when 'tomorrow' then $today.add(1)
31
31
  when /^\+([0-9]+)/ then $today + Regexp.last_match(1).to_i
32
32
  when /^-([0-9]+)/ then $today - Regexp.last_match(1).to_i
33
33
  else parse(str)
34
34
  end
35
35
  end
36
36
 
37
+ # Add a number of days accounting for daylight saving time changes
38
+ #
39
+ # @param days [Integer] Number of days to add
40
+ # @return [RDT] A new RDT
41
+ def add(days)
42
+ n = self + days
43
+ t = Time.parse("#{n.year}-#{n.month}-#{n.day} #{n.hour}:#{n.min}:#{n.sec}")
44
+ RDT.from_time(t)
45
+ end
46
+
37
47
  # Values can be +day before yesterday+, +yesterday+,
38
48
  # +today+, +tomorrow+, +day after tomorrow+, or the result from
39
49
  # strftime
@@ -41,10 +51,10 @@ module ICalPal
41
51
  # @return [String] A string representation of self relative to
42
52
  # today.
43
53
  def to_s
44
- return strftime($opts[:df]) if $opts && $opts[:nrd] && $opts[:df]
54
+ return strftime($opts[:df]) if $opts && $opts[:df] && $opts[:nrd]
45
55
  return super unless $today && $opts
46
56
 
47
- case Integer(RDT.new(*ymd, month, day) - $today)
57
+ case (self - $today).round
48
58
  when -2 then 'day before yesterday'
49
59
  when -1 then 'yesterday'
50
60
  when 0 then 'today'
@@ -68,19 +78,27 @@ module ICalPal
68
78
  to_time.to_i
69
79
  end
70
80
 
81
+ # @param z [Integer] Optional UTC offset
71
82
  # @return [RDT] Self at 00:00:00
72
- def day_start
73
- RDT.new(year, month, day, 0, 0, 0, zone)
83
+ def day_start(z = zone)
84
+ RDT.new(year, month, day, 0, 0, 0, z)
74
85
  end
75
86
 
87
+ # @param z [Integer] Optional UTC offset
76
88
  # @return [RDT] Self at 23:59:59
77
- def day_end
78
- RDT.new(year, month, day, 23, 59, 59, zone)
89
+ def day_end(z = zone)
90
+ RDT.new(year, month, day, 23, 59, 59, z)
79
91
  end
80
92
 
81
93
  # @return [Array] Only the year, month and day of self
82
94
  def ymd
83
95
  [ year, month, day ]
84
96
  end
97
+
98
+ # @return [Array] Only the hour, min and sec of self
99
+ def hms
100
+ [ hour, min, sec ]
101
+ end
102
+
85
103
  end
86
104
  end
data/lib/reminder.rb CHANGED
@@ -1,5 +1,3 @@
1
- r 'tzinfo'
2
-
3
1
  module ICalPal
4
2
  # Class representing items from the <tt>Reminders</tt> database
5
3
  class Reminder
@@ -56,6 +54,12 @@ module ICalPal
56
54
  when 'sdate' # For sorting
57
55
  @self['due_date']
58
56
 
57
+ when 'sday'
58
+ @self['due'].day_start(0) if @self['due']
59
+
60
+ when 'section'
61
+ (@self['section'])? @self['section'] : 'Others'
62
+
59
63
  when 'name', 'reminder', 'task' # Aliases
60
64
  @self['title']
61
65
 
@@ -67,11 +71,13 @@ module ICalPal
67
71
  super
68
72
 
69
73
  # Convert JSON arrays to Arrays
70
- @self['tags'] = JSON.parse(obj['tags']) if obj['tags']
71
- @self['location'] = JSON.parse(obj['location']).compact.uniq[0] if obj['location']
72
- @self['proximity'] = JSON.parse(obj['proximity']).compact.uniq[0] if obj['proximity']
73
- @self['radius'] = JSON.parse(obj['radius']).compact.uniq[0] if obj['radius']
74
- @self['assignee'] = JSON.parse(obj['assignee']) if obj['assignee']
74
+ %w[ assignee tags ].each do |a|
75
+ @self[a] = JSON.parse(obj[a]) if obj[a]
76
+ end
77
+
78
+ %w[ location proximity radius ].each do |a|
79
+ @self[a] = JSON.parse(obj[a]).compact.uniq[0] if obj[a]
80
+ end
75
81
 
76
82
  # Section
77
83
  if @self['members']
@@ -95,14 +101,9 @@ module ICalPal
95
101
 
96
102
  # Due date
97
103
  if @self['due_date']
98
- begin
99
- @self['due_date'] += ITIME
100
- zone = TZInfo::Timezone.get(@self['timezone'])
101
- rescue TZInfo::InvalidTimezoneIdentifier
102
- zone = '+00:00'
103
- end
104
-
105
- @self['due'] = RDT.from_time(Time.at(@self['due_date'], in: zone))
104
+ @self['due_date'] += ITIME
105
+ @self['due_date'] -= obj['utc_offset'] if obj['utc_offset']
106
+ @self['due'] = RDT.from_time(Time.at(@self['due_date']))
106
107
  end
107
108
 
108
109
  # Notes
@@ -141,6 +142,30 @@ module ICalPal
141
142
  @self['messaging'] = messaging
142
143
  end
143
144
 
145
+ # @see ICalPal.<=>
146
+ #
147
+ # When comparing sections, "Others" always goes last
148
+ def <=>(other)
149
+ $sort_attrs.each do |s|
150
+ next if self[s] == other[s]
151
+
152
+ # nil is always less than
153
+ return -1 if other[s].nil?
154
+ return 1 if self[s].nil?
155
+
156
+ if s == 'section'
157
+ # Section "Others" always goes last
158
+ return -1 if other[s] == 'Others'
159
+ return 1 if self[s] == 'Others'
160
+ end
161
+
162
+ return -1 if self[s] < other[s]
163
+ return 1 if self[s] > other[s]
164
+ end
165
+
166
+ 0
167
+ end
168
+
144
169
  DEFAULT_COLOR = '#1BADF8'.freeze
145
170
  DEFAULT_SYMBOLIC_COLOR = 'blue'.freeze
146
171
 
@@ -164,9 +189,9 @@ r1.zNotes as notes,
164
189
  r1.zPriority as priority,
165
190
  r1.zContactHandles as messaging,
166
191
  r1.zDueDateDeltaAlertsData as alert,
167
- r1.zTimezone as timezone,
168
192
  r1.zckIdentifier as id,
169
193
  r1.zCompleted as completed,
194
+ r1.zDisplayDateUpdatedForSecondsFromGMT as utc_offset,
170
195
 
171
196
  bl1.zBadgeEmblem as badge,
172
197
  bl1.zColor as color,
data/lib/store.rb CHANGED
@@ -37,8 +37,6 @@ s1.type,
37
37
 
38
38
  FROM #{self.name.split('::').last} s1
39
39
 
40
- WHERE s1.delegated_account_owner_store_id IS NULL
41
-
42
40
  SQL
43
41
 
44
42
  end
data/lib/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module ICalPal
2
2
  NAME = 'icalPal'.freeze
3
- VERSION = '3.9.3'.freeze
3
+ VERSION = '3.10.2'.freeze
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalPal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.3
4
+ version: 3.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Rosen
@@ -22,7 +22,6 @@ extra_rdoc_files:
22
22
  files:
23
23
  - README.md
24
24
  - bin/icalPal
25
- - bin/icalpal
26
25
  - ext/extconf.rb
27
26
  - icalPal.gemspec
28
27
  - lib/EventKit.rb
@@ -62,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
61
  - !ruby/object:Gem::Version
63
62
  version: '0'
64
63
  requirements: []
65
- rubygems_version: 3.7.2
64
+ rubygems_version: 3.6.9
66
65
  specification_version: 4
67
66
  summary: Command-line tool to query the macOS Calendar and Reminders
68
67
  test_files: []
data/bin/icalpal DELETED
@@ -1,361 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # rubocop: disable Style/RedundantBegin
4
-
5
- # require a gem
6
- #
7
- # @param gem [String] The gem
8
- def r(gem)
9
- begin
10
- # puts "require \"#{gem}\""
11
- require gem
12
- rescue LoadError => e
13
- $stderr.puts "FATAL: icalPal is missing a dependency: #{gem}"
14
- $stderr.puts e
15
- $stderr.puts
16
- abort "Try installing with 'gem install #{gem}'"
17
- end
18
- end
19
-
20
- # require_relative a library
21
- #
22
- # @param library [String] The library
23
- def rr(library)
24
- begin
25
- # puts "require_relative \"../lib/#{library}\""
26
- require_relative "../lib/#{library}"
27
- rescue LoadError => e
28
- $stderr.puts "FATAL: Could not load library: #{library}"
29
- $stderr.puts
30
- abort e.message
31
- end
32
- end
33
-
34
- # rubocop: enable Style/RedundantBegin
35
-
36
- %w[ logger csv json rdoc sqlite3 yaml ].each { |g| r g }
37
- %w[ icalPal defaults options utils ].each { |l| rr l }
38
-
39
-
40
- ##################################################
41
- # Load options
42
-
43
- # All kids love log!
44
- $log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
45
- $log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
46
- format("[%-5<sev>s] %<time>s [%<file>s:%<line>5s] - %<message>s\n",
47
- {
48
- sev: s,
49
- time: t.strftime('%H:%M:%S.%L'),
50
- file: caller(4, 1)[0].split('/')[-1].split(':')[0],
51
- line: caller(4, 1)[0].split('/')[-1].split(':')[1],
52
- message: m
53
- })
54
- end
55
-
56
- $opts = ICalPal::Options.new.parse_options
57
-
58
- $rows = [] # Rows from the database
59
- $sections = [] # Calendar list sections
60
- $items = [] # Items to be printed
61
-
62
-
63
- ##################################################
64
- # All kids love log!
65
-
66
- $log.info("Options:\n#{$opts.to_json}")
67
-
68
-
69
- ##################################################
70
- # Add an item to the list
71
- #
72
- # @param item[Object]
73
-
74
- def add(item)
75
- $log.debug("Adding #{item.dump} #{item['UUID']} (#{item['title']})") if item['UUID']
76
-
77
- $items.push(item)
78
- end
79
-
80
-
81
- ##################################################
82
- # Load the data
83
-
84
- # What are we getting?
85
- klass = ICalPal.call($opts[:cmd])
86
- success = false
87
-
88
- # Get it
89
- $opts[:db].each do |db|
90
- $log.debug("Trying #{db}")
91
-
92
- if klass == ICalPal::Reminder
93
- # Load all .sqlite files
94
- $log.debug("Loading *.sqlite in #{db}")
95
- Dir.glob("#{db}/*.sqlite").each do |d|
96
- success = true
97
-
98
- rows = klass.load_data(d, klass::QUERY)
99
- $rows += rows
100
-
101
- sections = klass.load_data(d, klass::SECTIONS_QUERY)
102
- $sections += sections
103
-
104
- $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
105
- end
106
- else
107
- # Load database
108
- rows = ICalPal.load_data(db, klass::QUERY)
109
- $rows += rows
110
-
111
- success = true
112
-
113
- $log.info("Loaded #{rows.length} rows from #{db}")
114
- end
115
-
116
- rescue Errno::EPERM
117
- # Probably need Full Disk Access
118
-
119
- rescue SQLite3::CantOpenException
120
- # Non-fatal exception, try the next one
121
-
122
- rescue StandardError => e
123
- # Log the error and (try to) continue
124
- $log.error("#{db}: #{e.message}")
125
- end
126
-
127
- # Make sure we opened at least one database
128
- unless success
129
- $log.fatal('Could not open database')
130
-
131
- # SQLite3 does not return useful error messages. If any databases
132
- # failed because of EPERM (operation not permitted), our parent
133
- # process might need Full Disk Access, and we should suggest that.
134
- eperm = 0
135
-
136
- $opts[:db].each do |db|
137
- # Use a real open to get a useful error
138
- File.open(db).close
139
- rescue Exception => e
140
- $log.fatal("#{e.class}: #{db}")
141
-
142
- eperm = 1 if e.instance_of?(Errno::EPERM)
143
- end
144
-
145
- if eperm.positive?
146
- $stderr.puts
147
- $stderr.puts "Does #{ancestor} have Full Disk Access in System Settings?"
148
- $stderr.puts
149
- $stderr.puts "Try running: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'"
150
- end
151
-
152
- abort
153
- end
154
-
155
- $log.debug("Loaded #{$rows.length} #{klass} items")
156
- $log.info("Window is #{$opts[:from]} to #{$opts[:to]}") if $opts[:from]
157
-
158
-
159
- ##################################################
160
- # Process the data
161
-
162
- # Add rows
163
- $rows.each do |row|
164
- # --es/--is
165
- next if $opts[:es].any? row['account']
166
- next unless $opts[:is].empty? || ($opts[:is].any? row['account'])
167
-
168
- # --ec/--ic
169
- unless klass == ICalPal::Store || !row['calendar']
170
- next if $opts[:ec].any? row['calendar']
171
- next unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
172
- end
173
-
174
- # Instantiate an item
175
- item = klass.new(row)
176
-
177
- # --et/--it
178
- next if $opts[:et].any? item['type']
179
- next unless $opts[:it].empty? || ($opts[:it].any? item['type'])
180
-
181
- # --el/--il
182
- next if $opts[:el].any? item['list_name']
183
- next unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
184
-
185
- # --match
186
- if $opts[:match]
187
- r = $opts[:match].split('=')
188
-
189
- if item[r[0]].to_s.respond_to?(:match)
190
- next unless item[r[0]].to_s =~ Regexp.new(r[1], Regexp::IGNORECASE)
191
- end
192
- end
193
-
194
- if item.is_a?(ICalPal::Event)
195
- # Check for all-day and cancelled events
196
- next if $opts[:ea] && item['all_day'].positive?
197
- next if $opts[:ia] && !item['all_day'].positive?
198
- next if item['status'] == :canceled
199
-
200
- (item['has_recurrences'].positive?)?
201
- item.recurring.each { |j| add(j) } :
202
- item.non_recurring.each { |j| add(j) }
203
- else
204
- # Check for completed or dated reminders
205
- if item.is_a?(ICalPal::Reminder)
206
- next if $opts[:ed] && item['completed'].zero?
207
- next unless $opts[:id] || item['completed'].zero?
208
-
209
- next if $opts[:dated] == 1 && item['due_date'] && item['due_date'].positive?
210
- next if $opts[:dated] == 2 && (!item['due_date'] || item['due_date'].zero?)
211
-
212
- if $opts[:dated] == 3
213
- next unless item['due_date']
214
- next unless item['due_date'].between?($opts[:from].to_i, $opts[:to].to_i)
215
- end
216
- end
217
-
218
- add(item)
219
- end
220
- end
221
-
222
- # Add placeholders for empty days
223
- if $opts[:sed] && $opts[:sd] && klass == ICalPal::Event
224
- days = $items.collect { |i| i['sday'] }.uniq.sort
225
-
226
- $opts[:days].times do |n|
227
- day = $opts[:from] + n
228
- $items.push(klass.new(day)) unless days.any? { |i| i.to_s == day.to_s }
229
- end
230
- end
231
-
232
- # Sort the rows
233
- begin
234
- sort = []
235
- sort.push $opts[:sep] if $opts[:sep]
236
- sort.push $opts[:sort] if $opts[:sort]
237
- sort.push 'sdate'
238
-
239
- $log.info("Sorting #{$items.count} items by #{sort}, reverse #{$opts[:reverse].inspect}")
240
-
241
- $items.sort_by! { |i| [ i[sort[0]], i[sort[1]], i[sort[2]] ] }
242
- $items.reverse! if $opts[:reverse]
243
- rescue Exception => e
244
- $log.info("Sorting failed: #{e}\n")
245
- end
246
-
247
- $log.debug("#{$items.count} items remain")
248
-
249
- # Configure formatting
250
- mu = case $opts[:output]
251
- when 'ansi' then RDoc::Markup::ToAnsi.new
252
- when 'default' then RDoc::Markup::ToICalPal.new($opts)
253
- when 'html'
254
- rdoc = RDoc::Options.new
255
- rdoc.pipe = true
256
- rdoc.output_decoration = false
257
- RDoc::Markup::ToHtml.new(rdoc)
258
- when 'md' then RDoc::Markup::ToMarkdown.new
259
- when 'rdoc' then RDoc::Markup::ToRdoc.new
260
- when 'toc' then RDoc::Markup::ToTableOfContents.new
261
- end
262
-
263
-
264
- ##################################################
265
- # Print the data
266
-
267
- items = $items[0..($opts[:li] - 1)]
268
-
269
- unless mu
270
- $log.debug("Output in #{$opts[:output]} format")
271
-
272
- puts case $opts[:output]
273
- when 'csv'
274
- # Get all headers
275
- headers = []
276
- items.each { |i| headers += i.keys }
277
- headers.uniq!
278
-
279
- # Populate a CSV::Table
280
- table = CSV::Table.new([], headers: headers)
281
- items.each { |i| table << i.to_csv(headers) }
282
-
283
- table
284
- when 'hash' then items.map { |i| i.self }
285
- when 'json' then items.map { |i| i.self }.to_json
286
- when 'xml'
287
- xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
288
- "<#{$opts[:cmd]}>\n#{xml.join}</#{$opts[:cmd]}>"
289
- when 'yaml' then items.map { |i| i.self }.to_yaml
290
- when 'remind' then items.map { |i|
291
- "REM #{i['sdate'].strftime('%F AT %R')} " +
292
- "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
293
- "MSG #{i['title'].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] }}"
294
- }.join("\n")
295
- else abort "No formatter for #{$opts[:output]}"
296
- end
297
-
298
- exit
299
- end
300
-
301
- $log.debug("Formatting with #{mu.inspect}")
302
-
303
- doc = RDoc::Markup::Document.new
304
- section = nil
305
-
306
- items.each_with_index do |i, j|
307
- $log.debug("Print #{j}: #{i.inspect}")
308
-
309
- # --li
310
- break if $opts[:li].positive? && j >= $opts[:li]
311
-
312
- # Use RDoc::Markup::Verbatim to save the item
313
- v = RDoc::Markup::Verbatim.new
314
- v.format = i
315
- doc << v
316
-
317
- # Sections
318
- if $opts[:sep] && section != i[$opts[:sep]]
319
- $log.debug("New section '#{$opts[:sep]}': #{i[$opts[:sep]]}")
320
-
321
- doc << RDoc::Markup::Raw.new($opts[:sep])
322
-
323
- doc << RDoc::Markup::BlankLine.new if j.positive?
324
- doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
325
- doc << RDoc::Markup::Rule.new(0)
326
-
327
- section = i[$opts[:sep]]
328
- end
329
-
330
- # Item
331
- props = RDoc::Markup::List.new(:BULLET)
332
-
333
- # Properties
334
- $opts[:props].each_with_index do |prop, k|
335
- value = i[prop]
336
-
337
- next unless value
338
- next if value.is_a?(Array) && !value[0]
339
- next if value.is_a?(String) && value.empty?
340
-
341
- $log.debug("#{prop}: #{value}")
342
-
343
- # Use Raw to save the property
344
- props << RDoc::Markup::Raw.new(prop)
345
-
346
- if k.positive?
347
- props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
348
- props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless (i['placeholder'])
349
- else
350
- # First property, value only
351
- props << RDoc::Markup::Heading.new(2, value.to_s)
352
- end
353
- end
354
-
355
- # Print it
356
- props << RDoc::Markup::BlankLine.new unless props.empty?
357
-
358
- doc << props
359
- end
360
-
361
- print doc.accept(mu)