ea 0.6.121 → 0.6.122

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: d27202e3cefb07cca1a7bc22923a692ef3eb882be925e4ec8c31962518620c07
4
- data.tar.gz: c31f63e7b1d97192c503d2a6b1464067aaff21dd06d8f716eeb83abc0f42a858
3
+ metadata.gz: 134a5c15aeb52f545cffe73a1541d36c404bc73daef03794c3cc0289744c0364
4
+ data.tar.gz: 231fad8c18f4f7ab8b72bc7e3f4b23652f2b44dfd5b9e6b54d8377cc3e4625d9
5
5
  SHA512:
6
- metadata.gz: 2a22e4c323e4e34f1e9b65ed0b3881dc6f8593d4e36546e64550cf050aeef603373d656850ae88d5992a2dc330371203fa07f6fab495378093a1c409e510114f
7
- data.tar.gz: 3923190f152ee52b99476f64c97e0201b783beea6291b18e8b1bb28967ef9a1f09188cf7bbad41d8a4339434471ea9e577d73cc4c1135f2dc88f0b1b1fdf650b
6
+ metadata.gz: 210b2520ec8a7d00782cc06b65a27ff3b391c611a4994b97aabc3281528db9e1356a96cd1b07a8734533f50c8c1b9a9e82e74576cf2a25f533c442e163ea09e4
7
+ data.tar.gz: d46dd157918d5fa7a1d9884d6cdba517a4d3fc911894c6f800d2cece0fdb62e2b5a09dfa030bd80cc604fe5ab2a91a310d86b5c9893c54b81cad2536200563ac
@@ -22,6 +22,8 @@ module Ea
22
22
  attribute :line_width, :integer
23
23
  attribute :font_family, :string
24
24
  attribute :font_size, :integer
25
+ # nil: unspecified provenance; true: source delegates to default resolution.
26
+ attribute :font_size_inherited, :boolean
25
27
  attribute :font_logical_height, :integer # original GDI pixel height before point normalization
26
28
  attribute :font_size_unit, :string # source font size unit, before theme fallback
27
29
  attribute :font_bold, :boolean
@@ -55,6 +57,7 @@ module Ea
55
57
  map "lineWidth", to: :line_width
56
58
  map "fontFamily", to: :font_family
57
59
  map "fontSize", to: :font_size
60
+ map "fontSizeInherited", to: :font_size_inherited, render_default: true
58
61
  map "fontSizeUnit", to: :font_size_unit
59
62
  map "fontLogicalHeight", to: :font_logical_height
60
63
  map "fontBold", to: :font_bold, render_default: true
@@ -138,6 +138,7 @@ module Ea
138
138
  style: style_hash,
139
139
  font_family: presence(font_style[:font]),
140
140
  font_size: font_size_from(font_style),
141
+ font_size_inherited: font_size_from(font_style).nil?,
141
142
  font_size_unit: "pt",
142
143
  font_logical_height: font_logical_height_from(font_style),
143
144
  font_bold: truthy?(style_hash[:bold]),
@@ -15,7 +15,7 @@ module Ea
15
15
  context.bounds,
16
16
  legend: context.model_element.legend,
17
17
  family: context.family,
18
- item_font: { size: context.element&.font_size&.nonzero?, unit: context.size_unit,
18
+ item_font: { size: legend_item_size(context), unit: context.size_unit,
19
19
  logical_height: context.logical_height }
20
20
  )
21
21
  elsif context.model_element.is_a?(Ea::Model::Note)
@@ -54,6 +54,17 @@ module Ea
54
54
  end
55
55
  end
56
56
 
57
+ def legend_item_size(context)
58
+ element = context.element
59
+ return unless element
60
+ return element.font_size if element.font_size&.nonzero?
61
+
62
+ # Parsed source defaults inherit; unspecified callers keep 9pt.
63
+ inherited = element.font_size_inherited || element.style&.fetch(:fontsz, nil).to_s == "0"
64
+ context.size if inherited
65
+ end
66
+ private_class_method :legend_item_size
67
+
57
68
  # A Text element with LegendOpts= renders as the legend
58
69
  # block rather than a folder silhouette. Dispatch on the
59
70
  # presence of the attached Legend payload.
@@ -50,14 +50,14 @@ module Ea
50
50
  end
51
51
 
52
52
  def layers
53
- widths = paths_with_widths
53
+ styles = paths_with_styles
54
54
 
55
55
  if grouped
56
- widths.group_by(&:first).map do |width, entries|
57
- line_layer(width, entries.map(&:last).join("\n "))
56
+ styles.group_by(&:first).map do |(width, dashed), entries|
57
+ line_layer(width, dashed, entries.map(&:last).join("\n "))
58
58
  end
59
59
  else
60
- widths.map { |width, path| line_layer(width, path) }
60
+ styles.map { |(width, dashed), path| line_layer(width, dashed, path) }
61
61
  end
62
62
  end
63
63
 
@@ -73,16 +73,29 @@ module Ea
73
73
 
74
74
  private
75
75
 
76
- def paths_with_widths
76
+ def paths_with_styles
77
77
  visible_connectors.filter_map do |connector|
78
78
  path = path_for(connector)
79
- [self.class.stroke_width_for(connector, stroke_width), path] if path
79
+ next unless path
80
+
81
+ [[self.class.stroke_width_for(connector, stroke_width), dashed?(connector)], path]
80
82
  end
81
83
  end
82
84
 
83
- def line_layer(width, body)
84
- Layer.new(style_key: self.class.layer_key(LINE_STYLE_KEY, width, stroke_width),
85
- style: line_style(width), body: body)
85
+ def line_layer(width, dashed, body)
86
+ key = dashed ? :connector_line_dashed : LINE_STYLE_KEY
87
+ style = line_style(width)
88
+ style += ";stroke-dasharray:6,3" if dashed
89
+ Layer.new(style_key: self.class.layer_key(key, width, stroke_width),
90
+ style: style, body: body)
91
+ end
92
+
93
+ def dashed?(connector)
94
+ return true if connector.connector_type == "Dependency"
95
+ return false unless connector.connector_type == "Package"
96
+
97
+ relationship = model_index && model_index[connector.relationship_ref]
98
+ relationship&.stereotype == "import"
86
99
  end
87
100
 
88
101
  def visible_connectors
@@ -10,13 +10,12 @@ module Ea
10
10
  # type.
11
11
  #
12
12
  # Layout (element bounds cover tab + body together; the tab
13
- # occupies the TOP of the stored rect — corpus-verified
14
- # against EA-published package polygons, single tab 20px and
15
- # stereotype tab 36px, both inside):
13
+ # occupies the TOP of the stored rect, with its height adjusted
14
+ # by the font line height for each header row):
16
15
  #
17
16
  # ┌───┐
18
- # │tab│ ← top TAB_HEIGHT rows of the bounds;
19
- # │ │ 36px when a stereotype is present
17
+ # │tab│ ← one name row, or two rows with a stereotype
18
+ # │ │
20
19
  # ├───┴──────────┐
21
20
  # │ │ the name renders bold, left-aligned;
22
21
  # │ body │ the body always starts where the
@@ -148,7 +147,11 @@ module Ea
148
147
  end
149
148
 
150
149
  def tab_height
151
- has_stereotype? ? TAB_HEIGHT_DOUBLE : TAB_HEIGHT_SINGLE
150
+ nominal_height = has_stereotype? ? TAB_HEIGHT_DOUBLE : TAB_HEIGHT_SINGLE
151
+ rows = has_stereotype? ? 2 : 1
152
+ # Preserve the nominal padding while each row follows the same
153
+ # font line height used to place the package header text.
154
+ nominal_height + rows * (tab_line_height - TAB_LABEL_Y_OFFSET)
152
155
  end
153
156
 
154
157
  def body_points
@@ -152,8 +152,10 @@ module Ea
152
152
  []
153
153
  end
154
154
  # Titles wrap; content rows grow the drawn box to fit their insets.
155
- row_w = [row_content_width(attr_lines, 5, size, family, logical_height),
156
- row_content_width(op_lines, 3, size, family, logical_height),
155
+ row_w = [row_content_width(attr_lines, 5, size, family,
156
+ logical_height: logical_height,
157
+ content_x_offset: attribute_content_x_offset(size, family, size_unit)),
158
+ row_content_width(op_lines, 3, size, family, logical_height: logical_height),
157
159
  Compartment::EnumLiterals.content_width(
158
160
  enum_literals_for(classifier),
159
161
  theme: theme, size: size, family: family,
@@ -249,7 +251,8 @@ module Ea
249
251
  # Real attributes grow the DRAWN box past the stored
250
252
  # height instead of being dropped (0CABA7D7: stored h44
251
253
  # draws at h59). Grown height = last row baseline + EA's
252
- # ~8px bottom padding. Render-local: the diagram-level
254
+ # trailing padding (7px for compact Cambria, 8px otherwise).
255
+ # Render-local: the diagram-level
253
256
  # canvas still derives from stored bounds.
254
257
  # content_bottom_y is nil when no compartment has rows;
255
258
  # op_bottom_y is NOT a sentinel (it reports the op-divider
@@ -268,7 +271,7 @@ module Ea
268
271
  bounds = Ea::Model::Bounds.new(
269
272
  x: bounds.x, y: bounds.y,
270
273
  width: bounds.width,
271
- height: grown_bottom + 8 - bounds.y
274
+ height: grown_bottom + compartment_bottom_padding(size, family, size_unit) - bounds.y
272
275
  )
273
276
  end
274
277
  RenderContext.new(
@@ -298,18 +301,30 @@ module Ea
298
301
  )
299
302
  end
300
303
 
301
- def row_content_width(lines, right_pad, size, family, logical_height)
304
+ def attribute_content_x_offset(size, family, size_unit)
305
+ offset = (theme.attribute_spec.content_x_offset || Element::AttributeRenderer::DEFAULT_CONTENT_X_OFFSET) +
306
+ 2 * (size - 7)
307
+ offset -= 1 if Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit)
308
+ offset
309
+ end
310
+
311
+ def compartment_bottom_padding(size, family, size_unit)
312
+ Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit) ? 7 : 8
313
+ end
314
+
315
+ def row_content_width(lines, right_pad, size, family, **font_options)
302
316
  return nil if lines.to_a.empty?
303
317
 
304
- tl = lines.filter_map do |line|
318
+ lines.filter_map do |line|
305
319
  text = line.sub(/\A\S /, "")
306
- Ea::Fonts::Metrics.text_length(text, size,
307
- family: family,
308
- weight: "400", logical_height: logical_height)&.round
309
- end.max
310
- return nil unless tl
320
+ length = Ea::Fonts::Metrics.text_length(text, size,
321
+ family: family,
322
+ weight: "400", logical_height: font_options[:logical_height])&.round
323
+ next unless length
311
324
 
312
- 22 + tl + right_pad
325
+ offset = line.match?(/\A[-+~#]\s/) ? font_options.fetch(:content_x_offset, 22) : 22
326
+ offset + length + right_pad
327
+ end.max
313
328
  end
314
329
 
315
330
  def compartment_geometry(bounds, size, attr_lines, op_lines,
@@ -79,6 +79,7 @@ module Ea
79
79
  text_pos, mult_pos = tree_positions(end_kind, anchor, role,
80
80
  text_pos, mult_pos)
81
81
  mult_pos = tree_left_multiplicity(end_kind, heading, anchor, mult_box, mult) || mult_pos
82
+ text_pos = tree_left_property_role(connector, end_kind, heading, anchor, text_box, role) || text_pos
82
83
  elsif mult_box && stored_offsets_zero?(mult_box)
83
84
  # Start at the legacy default; measured directional seats
84
85
  # below replace it for default Cambria labels.
@@ -210,8 +211,9 @@ module Ea
210
211
 
211
212
  def source_property_seat?(connector, direction, box)
212
213
  phantom_property_box?(connector, direction, box) ||
213
- (%i[up left right down].include?(direction) && property_role_box?(box, direction) &&
214
- source_property_font?(connector, direction))
214
+ (%i[up left right down].include?(direction) &&
215
+ property_role_box?(box, direction, connector.style.to_h) &&
216
+ source_property_font?(connector, direction, box))
215
217
  end
216
218
 
217
219
  # Phantom outlines carry synthesized zero boxes without alignment.
@@ -392,9 +394,23 @@ module Ea
392
394
  [nil, "V"].include?(style[:tree])
393
395
  end
394
396
 
395
- def property_role_box?(box, direction)
396
- alignments = direction == :down ? [1] : [0, 1]
397
- box && alignments.include?(box["alignment"]) && default_presentation?(box)
397
+ def property_role_box?(box, direction, style = {})
398
+ return false unless box && default_presentation?(box)
399
+ return [0, 1].include?(box["alignment"]) unless direction == :down
400
+
401
+ case box["alignment"]
402
+ when 1 then true
403
+ when 0 then persisted_plain_down_box?(box, style)
404
+ else false
405
+ end
406
+ end
407
+
408
+ # Persisted alignment-0 boxes on untreed routes use the downward
409
+ # seat, including saved rays; the "OS" route also seats absent boxes.
410
+ def persisted_plain_down_box?(box, style)
411
+ return true if orthogonal_stub_route?(style)
412
+
413
+ !stored_offsets_zero?(box) && style[:tree].nil?
398
414
  end
399
415
 
400
416
  def property_role_offsets(direction)
@@ -405,26 +421,58 @@ module Ea
405
421
  end
406
422
  end
407
423
 
408
- def source_property_font?(connector, direction)
424
+ def source_property_font?(connector, direction, box)
409
425
  source_property_cambria?(connector) &&
410
- source_property_route?(connector.style.to_h, direction)
426
+ source_property_route?(connector.style.to_h, direction, box)
411
427
  end
412
428
 
413
429
  def source_property_cambria?(connector)
414
430
  font_family.to_s.casecmp("Cambria").zero? && property_label?(connector, :source)
415
431
  end
416
432
 
417
- def source_property_route?(style, direction)
433
+ def source_property_route?(style, direction, box)
418
434
  case direction
419
- when :left then left_property_route?(style)
420
435
  when :right then style.values_at(:mode, :lwidth, :tree) == %w[3 2 H]
421
- when :down then downward_property_route?(style)
422
- else %w[0 2].include?(style[:lwidth]) && style[:tree] != "OS"
436
+ when :up then upward_property_route?(style)
437
+ when :left then orthogonal_stub_route?(style) || left_property_route?(style)
438
+ else downward_source_route?(style, box)
423
439
  end
424
440
  end
425
441
 
442
+ def downward_source_route?(style, box)
443
+ [orthogonal_stub_route?(style), lateral_horizontal_route?(style), mode_two_route?(style),
444
+ downward_property_route?(style), persisted_width_zero_down_box?(box, style)].any?
445
+ end
446
+
447
+ def persisted_width_zero_down_box?(box, style)
448
+ box["alignment"]&.zero? && !stored_offsets_zero?(box) && style[:stored_association_ray] != true &&
449
+ style.values_at(:mode, :lwidth, :tree) == ["3", "0", nil]
450
+ end
451
+
452
+ # Mode 3, width 2 on the horizontal lateral tree.
453
+ def lateral_horizontal_route?(style)
454
+ style.values_at(:mode, :lwidth, :tree) == %w[3 2 LH]
455
+ end
456
+
457
+ # Mode 2, width 2 without a tree seats like the other width-2 modes.
458
+ def mode_two_route?(style)
459
+ style.values_at(:mode, :lwidth, :tree) == ["2", "2", nil]
460
+ end
461
+
462
+ def upward_property_route?(style)
463
+ return orthogonal_stub_route?(style) if style[:tree] == "OS"
464
+
465
+ %w[0 2].include?(style[:lwidth])
466
+ end
467
+
468
+ # The "OS" route seats its Property roles at the same directional
469
+ # corner seats as the untreed width-2 route.
470
+ def orthogonal_stub_route?(style)
471
+ style.values_at(:mode, :lwidth, :tree) == %w[3 2 OS]
472
+ end
473
+
426
474
  def left_property_route?(style)
427
- downward_property_route?(style) ||
475
+ downward_property_route?(style) || mode_two_route?(style) ||
428
476
  (style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) &&
429
477
  [nil, "H", "LV"].include?(style[:tree]))
430
478
  end
@@ -456,6 +504,15 @@ module Ea
456
504
  [x + SEAT_X_INSET + width, y + 15] if width
457
505
  end
458
506
 
507
+ # An absent source box on a leftward tree leg seats a Property role
508
+ # at the leftward Property seat, not the rightward tree default.
509
+ def tree_left_property_role(connector, end_kind, heading, anchor, box, role)
510
+ return nil unless leg_direction(end_kind, heading) == :left && box&.[]("alignment")&.zero?
511
+ return nil unless connector.style.to_h.values_at(:mode, :lwidth) == %w[3 2]
512
+
513
+ source_property_seat(connector, :left, anchor, box, role)
514
+ end
515
+
459
516
  # A tree-route source leaving left seats its multiplicity like any
460
517
  # other leftward source rather than at the rightward tree default.
461
518
  def tree_left_multiplicity(end_kind, heading, anchor, box, mult)
@@ -125,7 +125,7 @@ module Ea
125
125
  # Unknown faces retain the stored-upward fallback.
126
126
  def source_heading(connector, points)
127
127
  heading = leg_heading(points)
128
- return heading unless points.size == 2 && heading.none?(&:zero?)
128
+ return heading unless heading.none?(&:zero?) && source_face_route?(connector, points)
129
129
 
130
130
  box = source_dock_bounds(connector)
131
131
  face = box && source_dock_heading(connector, points.first, box)
@@ -134,6 +134,15 @@ module Ea
134
134
  stored_upward_heading(connector, heading)
135
135
  end
136
136
 
137
+ # Explicit source boxes preserve the heading of a manual bend. An
138
+ # omitted source box uses the same dock seat as a straight route.
139
+ def source_face_route?(connector, points)
140
+ return true if points.size == 2
141
+
142
+ boxes = connector.label_boxes || {}
143
+ end_boxes(boxes, :llt, :llb).nil? && !end_boxes(boxes, :lrt, :lrb).nil?
144
+ end
145
+
137
146
  # A target dock that clips an axis-aligned stored leg into a slight
138
147
  # diagonal keeps the stored leg's direction.
139
148
  def target_heading(connector, points)
@@ -150,15 +159,24 @@ module Ea
150
159
  # A saved external ray enters the source through the face it first
151
160
  # meets, even when the clipped leg then runs back across the box.
152
161
  def face_leads_leg?(connector, face, heading, box)
153
- anchor = connector.source_anchor
154
- if face == [0, 1] && anchor && anchor.y > box.y + box.height &&
155
- connector.style.to_h[:stored_association_ray] == true
156
- return true
157
- end
162
+ return true if saved_ray_beyond_face?(connector, face, box)
158
163
 
159
164
  face.zip(heading).any? { |axis, delta| (axis * delta).positive? }
160
165
  end
161
166
 
167
+ # A saved ray from beyond the bottom or right face meets that face
168
+ # first. The top face is excluded: its clipped leg keeps the anchor seat.
169
+ def saved_ray_beyond_face?(connector, face, box)
170
+ anchor = connector.source_anchor
171
+ return false unless anchor && connector.style.to_h[:stored_association_ray] == true
172
+
173
+ case face
174
+ when [0, 1] then anchor.y > box.y + box.height
175
+ when [1, 0] then anchor.x > box.x + box.width
176
+ else false
177
+ end
178
+ end
179
+
162
180
  def stored_upward_heading(connector, heading)
163
181
  return heading unless heading[1].negative?
164
182
 
@@ -20,6 +20,7 @@ module Ea
20
20
  anchor: whole_anchor,
21
21
  base: whole_base)]
22
22
  return specs unless whole_at_target
23
+ return specs if arrow_on_target_diamond?(connector, relationship)
23
24
 
24
25
  specs.concat(direction_specs(connector, source, target,
25
26
  before_target, after_source))
@@ -67,6 +68,14 @@ module Ea
67
68
  !whole_end_at_source?(connector)
68
69
  end
69
70
 
71
+ # EA draws no arrow where a directed, dest-flagged aggregation
72
+ # would put it on its own target diamond.
73
+ def self.arrow_on_target_diamond?(connector, relationship)
74
+ connector.direction == "Source -> Destination" &&
75
+ dest_flagged?(relationship)
76
+ end
77
+ private_class_method :arrow_on_target_diamond?
78
+
70
79
  def self.dest_flagged?(relationship)
71
80
  relationship &&
72
81
  %w[shared composite].include?(relationship.target_aggregation.to_s)
@@ -22,7 +22,7 @@ module Ea
22
22
  DIAMOND_HALF_W = 5
23
23
  DIAMOND_HALF_H = 10
24
24
  TRI_HALF_BASE = 6
25
- TRI_HEIGHT = 11
25
+ TRI_HEIGHT = 16
26
26
  # EA's path arrowheads are height 15 corpus-wide, with
27
27
  # distinct bases per connector kind: association
28
28
  # navigability arrows base 12, dependency arrows base 10
@@ -364,10 +364,10 @@ module Ea
364
364
  left_x = tx - ux * DIAMOND_HALF_H - (-uy) * DIAMOND_HALF_W
365
365
  left_y = ty - uy * DIAMOND_HALF_H - ux * DIAMOND_HALF_W
366
366
  pts = [
367
- Canvas.coord(tx), Canvas.coord(ty),
368
- Canvas.coord(right_x), Canvas.coord(right_y),
369
- Canvas.coord(back_x), Canvas.coord(back_y),
370
- Canvas.coord(left_x), Canvas.coord(left_y)
367
+ Canvas.coord(tx.round), Canvas.coord(ty.round),
368
+ Canvas.coord(right_x.round), Canvas.coord(right_y.round),
369
+ Canvas.coord(back_x.round), Canvas.coord(back_y.round),
370
+ Canvas.coord(left_x.round), Canvas.coord(left_y.round)
371
371
  ].join(" ")
372
372
  %(<polygon points="#{pts}" shape-rendering="auto" style="fill-rule:evenodd;"/>)
373
373
  end
@@ -385,9 +385,9 @@ module Ea
385
385
  w2_x = back_x - (-uy) * TRI_HALF_BASE
386
386
  w2_y = back_y - ux * TRI_HALF_BASE
387
387
  pts = [
388
- Canvas.coord(tx), Canvas.coord(ty),
389
- Canvas.coord(w1_x), Canvas.coord(w1_y),
390
- Canvas.coord(w2_x), Canvas.coord(w2_y)
388
+ Canvas.coord(tx.round), Canvas.coord(ty.round),
389
+ Canvas.coord(w1_x.round), Canvas.coord(w1_y.round),
390
+ Canvas.coord(w2_x.round), Canvas.coord(w2_y.round)
391
391
  ].join(" ")
392
392
  %(<polygon points="#{pts}" shape-rendering="auto" style="fill-rule:evenodd;"/>)
393
393
  end
@@ -404,7 +404,9 @@ module Ea
404
404
  w1_y = back_y + ux * ARROW_HALF_BASE
405
405
  w2_x = back_x - (-uy) * ARROW_HALF_BASE
406
406
  w2_y = back_y - ux * ARROW_HALF_BASE
407
- d = "M #{Canvas.coord(w1_x)} #{Canvas.coord(w1_y)} L #{Canvas.coord(tx)} #{Canvas.coord(ty)} L #{Canvas.coord(w2_x)} #{Canvas.coord(w2_y)}"
407
+ d = "M #{Canvas.coord(w1_x.round)} #{Canvas.coord(w1_y.round)} " \
408
+ "L #{Canvas.coord(tx.round)} #{Canvas.coord(ty.round)} " \
409
+ "L #{Canvas.coord(w2_x.round)} #{Canvas.coord(w2_y.round)}"
408
410
  %(<path d="#{d}" shape-rendering="auto"/>)
409
411
  end
410
412
 
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.6.121"
7
+ VERSION = "0.6.122"
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.121
4
+ version: 0.6.122
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.