rsyntaxtree 1.13.2 → 2.0.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.
data/lib/rsyntaxtree.rb CHANGED
@@ -34,11 +34,9 @@ WHITESPACE_BLOCK = "■"
34
34
  # Every format the library can produce, and the file extension each one
35
35
  # writes. The CLI builds its --format validation, help text, and output
36
36
  # filename from these, so a new format is added here and nowhere else.
37
- FORMATS = %w[png jpg gif pdf svg lsif tikz].freeze
37
+ FORMATS = %w[png pdf svg lsif tikz].freeze
38
38
  FORMAT_EXTENSIONS = {
39
39
  "png" => "png",
40
- "jpg" => "jpg",
41
- "gif" => "gif",
42
40
  "pdf" => "pdf",
43
41
  "svg" => "svg",
44
42
  "lsif" => "lsif.json",
@@ -54,7 +52,7 @@ FORMAT_EXTENSIONS = {
54
52
  # pass their own extra parameters through.
55
53
  OPTION_VALUES = {
56
54
  format: FORMATS,
57
- leafstyle: %w[auto triangle bar nothing none],
55
+ leafstyle: %w[auto triangle bar nothing],
58
56
  fontstyle: ["sans", "serif", "cjk", "mono", "noto-sans", "noto-serif", "noto-sans-mono", "cjk zenhei"],
59
57
  color: %w[modern traditional gray grey off none on true false],
60
58
  tidy: %w[off symmetric low medium high on compact true false],
@@ -68,7 +66,20 @@ NUMERIC_RANGES = {
68
66
  linewidth: 0.5..3.0,
69
67
  vheight: 0.5..5.0,
70
68
  hspacing: 0.5..3.0,
71
- tidy_spacing: 0.5..3.0
69
+ shear: -45..45,
70
+ vmargin: 0.1..1.0
71
+ }.freeze
72
+
73
+ # Options 2.0 removed, and what to say instead. An unknown key is passed over
74
+ # in silence, because a caller may hand its own parameters through — which is
75
+ # right for a key this library never had, and wrong for one it used to obey.
76
+ # A program that still asks for symmetrize would have been given the layout it
77
+ # asked for in 1.x and a different one here, with nothing said. Named here,
78
+ # the ask is refused the way `-f jpg` and `leafstyle: none` are.
79
+ REMOVED_OPTIONS = {
80
+ symmetrize: 'tidy: "symmetric"',
81
+ tidy_spacing: "hspacing",
82
+ tidy_nest: 'tidy: "high"'
72
83
  }.freeze
73
84
 
74
85
  DEFAULT_OPTS = {
@@ -79,7 +90,6 @@ DEFAULT_OPTS = {
79
90
  linewidth: 1,
80
91
  vheight: 2.0,
81
92
  color: "modern",
82
- symmetrize: "off",
83
93
  transparent: "off",
84
94
  polyline: "off",
85
95
  hide_default_connectors: "off",
@@ -88,7 +98,14 @@ DEFAULT_OPTS = {
88
98
  hspacing: 1.0,
89
99
  direction: "ttb",
90
100
  hyphen: "markup",
91
- derivation: "off"
101
+ derivation: "off",
102
+ shear: 0,
103
+ shear_plane: "on",
104
+ # 0.4 keeps the level pitch the historical drawing had, and replaces its
105
+ # unequal air — a little more below every label than above it, an
106
+ # unadjusted leftover of measuring from the layout box — with the same
107
+ # clearance on both sides of the ink.
108
+ vmargin: 0.4
92
109
  }.freeze
93
110
 
94
111
  # A parse or generation failure. `message` is the human-readable text the
@@ -163,12 +180,29 @@ module RSyntaxTree
163
180
  next if (OPTION_VALUES.key?(key) || NUMERIC_RANGES.key?(key)) &&
164
181
  (value.nil? || value.to_s.strip.empty?)
165
182
 
183
+ if REMOVED_OPTIONS.key?(key)
184
+ raise RSTError.new(+"Error: option '#{key}' was removed in RSyntaxTree 2.0",
185
+ code: :invalid_option,
186
+ hint: "Use #{REMOVED_OPTIONS[key]} instead.",
187
+ retryable: false)
188
+ end
166
189
  if OPTION_VALUES.key?(key) && !OPTION_VALUES[key].include?(value.to_s)
167
190
  raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
168
191
  code: :invalid_option,
169
192
  hint: "'#{key}' must be one of: #{OPTION_VALUES[key].join(', ')}.",
170
193
  retryable: false)
171
194
  end
195
+ # The range check reads the value as a float, and a string that is
196
+ # not a number reads as zero. Every range used to sit clear of zero,
197
+ # so nonsense failed the range check by accident; shear's runs through
198
+ # it, and "abc" would have been taken as no shear at all.
199
+ if NUMERIC_RANGES.key?(key) && !value.is_a?(Numeric) &&
200
+ value.to_s.strip !~ /\A-?(\d+(\.\d+)?|\.\d+)\z/
201
+ raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
202
+ code: :invalid_option,
203
+ hint: "'#{key}' takes a number.",
204
+ retryable: false)
205
+ end
172
206
  if NUMERIC_RANGES.key?(key) && !NUMERIC_RANGES[key].cover?(value.to_f)
173
207
  range = NUMERIC_RANGES[key]
174
208
  raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
@@ -200,10 +234,10 @@ module RSyntaxTree
200
234
  # strict leaf positions), "medium" (packing that may tuck
201
235
  # branches across rows as long as no two leaves swap their
202
236
  # left-right order), "high" (free tucking; leaf order kept per
203
- # row only). "on"/"compact" are accepted as legacy aliases of
204
- # low/high, legacy tidy_nest: "on" upgrades low to high, and
205
- # legacy symmetrize: "on" upgrades "off" to "symmetric" (both
206
- # below, in BaseGraph).
237
+ # row only). "on"/"compact" are accepted as aliases of low/high.
238
+ # The separate symmetrize and tidy_nest flags this scale replaced
239
+ # were removed in 2.0; tidy: symmetric and tidy: high say what
240
+ # they said.
207
241
  new_params[key] = case value.to_s
208
242
  when "high", "compact"
209
243
  "high"
@@ -216,7 +250,7 @@ module RSyntaxTree
216
250
  else
217
251
  "off"
218
252
  end
219
- when :tidy_nest, :symmetrize, :transparent, :polyline, :hide_default_connectors, :mirror, :derivation
253
+ when :transparent, :polyline, :hide_default_connectors, :mirror, :derivation
220
254
  new_params[key] = switched_on?(value)
221
255
  when :color
222
256
  new_params[key] = case value.to_s
@@ -235,8 +269,28 @@ module RSyntaxTree
235
269
  new_params[key] = value.to_i
236
270
  when :linewidth
237
271
  new_params[key] = value.to_f
238
- when :vheight, :hspacing, :tidy_spacing
272
+ when :vheight, :hspacing, :shear, :vmargin
239
273
  new_params[key] = value.to_f
274
+ when :shear_plane
275
+ # on | off | a colour. The empty string is a form control nobody
276
+ # touched, so the default stands — this key sits outside the value
277
+ # tables the blanket blank-skip above covers.
278
+ v = value.to_s.strip
279
+ unless v.empty?
280
+ new_params[key] = case v.downcase
281
+ when "on", "true", "yes", "1" then "on"
282
+ when "off", "false", "no", "0", "none" then "off"
283
+ else
284
+ unless COLOR_NAMES.include?(v.downcase) || v =~ /\A#(\h{3}|\h{6})\z/
285
+ raise RSTError.new(+"Error: invalid value for option 'shear_plane': #{value.inspect}",
286
+ code: :invalid_option,
287
+ hint: "'shear_plane' is on, off, a colour name, " \
288
+ "or a hex colour of 3 or 6 digits.",
289
+ retryable: false)
290
+ end
291
+ v
292
+ end
293
+ end
240
294
  when :fontstyle
241
295
  # Fonts are resolved by name through fontconfig (measurement via
242
296
  # Pango, rendering via the SVG font-family attribute), so all a
@@ -254,13 +308,6 @@ module RSyntaxTree
254
308
  end
255
309
  end
256
310
 
257
- # Legacy alias: tidy_spacing was the tidy-only horizontal gap factor
258
- # before it was generalized to hspacing (all layout modes).
259
- if (new_params[:hspacing].nil? || new_params[:hspacing] == 1.0) &&
260
- new_params[:tidy_spacing] && new_params[:tidy_spacing] != 1.0
261
- new_params[:hspacing] = new_params[:tidy_spacing]
262
- end
263
-
264
311
  # defaults to the following
265
312
  @params = DEFAULT_OPTS.dup
266
313
  @params.merge! new_params
@@ -313,8 +360,26 @@ module RSyntaxTree
313
360
  # Horizontal counterpart of vheight: hspacing scales every horizontal
314
361
  # gap (sibling clearance in all layout modes, tidy minimum gap,
315
362
  # margins) the way vheight scales the vertical rhythm.
316
- @global[:h_gap_between_nodes] = single_x_metrics.width * 0.8 * (@params[:hspacing] || 1.0).to_f
317
- @global[:box_vertical_margin] = single_x_metrics.height * 0.8
363
+ #
364
+ # The factor is kept beside the gap it scales because the left-to-right
365
+ # layout replaces that gap with one of its own and has to scale it by
366
+ # the same amount. Recovering the factor by dividing the gap back out
367
+ # would be this rule written a second time, free to drift from this one.
368
+ @global[:hspacing] = (@params[:hspacing] || 1.0).to_f
369
+ @global[:h_gap_between_nodes] = single_x_metrics.width * 0.8 * @global[:hspacing]
370
+ # The font's band: how far a capital reaches above the baseline and a
371
+ # descender below it. What the symmetric clearances of vmargin measure
372
+ # from — the band rather than each label's own ink, so the line ends
373
+ # line up across a level instead of following every g and y.
374
+ @global[:cap_height] = single_x_metrics.ink_above
375
+ xg_metrics = FontMetrics.get_metrics("Xg", fontset[:family], @params[:fontsize], :normal, :normal)
376
+ @global[:descender] = xg_metrics.ink_height - xg_metrics.ink_above
377
+ @global[:vmargin] = @params[:vmargin]
378
+ @global[:box_vertical_margin] = if @params[:vmargin]
379
+ single_x_metrics.height * @params[:vmargin] * 2
380
+ else
381
+ single_x_metrics.height * 0.8
382
+ end
318
383
  # Every stroke follows the type size: linewidth 1 is 5% of it (the
319
384
  # ratio of an ordinary text rule, booktabs' \lightrulewidth), each
320
385
  # 0.5 step of the option adds another 2.5%, and a bold stroke adds
@@ -386,17 +451,15 @@ module RSyntaxTree
386
451
  #
387
452
  # Which generation depends on the format asked for, because the formats
388
453
  # can refuse different things: a tree can be too wide for a raster
389
- # surface while remaining a perfectly good SVG, and JPG and GIF need a
390
- # gem that may not be installed. Everything up to the last point where
391
- # a format is known to say no is done; painting the tree onto the
392
- # surface, which is most of the cost and refuses nothing this knows of,
393
- # is not. A surface inside Cairo's limits but large enough to exhaust
394
- # memory would still fail at that painting, so this is where validation
395
- # is a strong guess rather than a guarantee.
454
+ # surface while remaining a perfectly good SVG. Everything up to the
455
+ # last point where a format is known to say no is done; painting the
456
+ # tree onto the surface, which is most of the cost and refuses nothing
457
+ # this knows of, is not. A surface inside Cairo's limits but large
458
+ # enough to exhaust memory would still fail at that painting, so this is
459
+ # where validation is a strong guess rather than a guarantee.
396
460
  def validate!
397
461
  case @params[:format]
398
462
  when "png" then raster_surface_for(draw_svg, &:finish)
399
- when "jpg", "gif" then with_rmagick { raster_surface_for(draw_svg, &:finish) }
400
463
  when "pdf" then pdf_surface_for(draw_svg, StringIO.new, &:finish)
401
464
  when "lsif" then draw_lsif
402
465
  when "tikz" then draw_tikz
@@ -472,44 +535,6 @@ module RSyntaxTree
472
535
  graph.svg_data
473
536
  end
474
537
 
475
- # JPG and GIF output converts the PNG through RMagick. The require is
476
- # lazy: both formats are deprecated and will be removed in 2.0, and the
477
- # library must load without RMagick for every other format. A missing
478
- # RMagick is reported as an input-level error, not a bare LoadError.
479
- def with_rmagick
480
- require 'rmagick'
481
- yield
482
- rescue LoadError
483
- raise RSTError.new(+"Error: JPG/GIF output requires ImageMagick and the rmagick gem, " \
484
- "which is not installed. Use PNG instead — JPG and GIF support " \
485
- "is deprecated and will be removed in 2.0.",
486
- code: :missing_dependency, retryable: false)
487
- end
488
-
489
- def draw_jpg
490
- with_rmagick do
491
- png_data = draw_png
492
- images = Magick::Image.from_blob(png_data)
493
- image = images.first
494
- image.format = 'JPEG'
495
- blob = image.to_blob
496
- images.each(&:destroy!)
497
- blob
498
- end
499
- end
500
-
501
- def draw_gif
502
- with_rmagick do
503
- png_data = draw_png
504
- images = Magick::Image.from_blob(png_data)
505
- image = images.first
506
- image.format = 'GIF'
507
- blob = image.to_blob
508
- images.each(&:destroy!)
509
- blob
510
- end
511
- end
512
-
513
538
  def draw_lsif
514
539
  sp = StringParser.new(@params[:data].gsub('&', '&'), @params[:fontset], @params[:fontsize], @global)
515
540
  sp.parse
data/rsyntaxtree.gemspec CHANGED
@@ -28,15 +28,16 @@ Gem::Specification.new do |s|
28
28
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
29
29
  s.require_paths = ["lib"]
30
30
 
31
- # specify any dependencies here; for example:
31
+ # Four, and each is used: optimist parses the command line, parslet parses
32
+ # the markup inside a label, pango measures the text, rsvg2 draws the PNG
33
+ # and the PDF. Two of them build against a system library — pango and
34
+ # librsvg — which is what an install of this gem asks a machine for.
32
35
  s.add_runtime_dependency "optimist", ">= 3.0.1"
33
36
  s.add_runtime_dependency "pango"
34
37
  s.add_runtime_dependency "parslet"
35
- s.add_runtime_dependency "rmagick", ">= 4.3"
36
38
  s.add_runtime_dependency "rsvg2"
37
39
 
38
40
  s.add_development_dependency "minitest"
39
41
  s.add_development_dependency "nokogiri"
40
42
  s.add_development_dependency "rake"
41
- s.add_development_dependency "yaml"
42
43
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsyntaxtree
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoichiro Hasebe
@@ -51,20 +51,6 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
- - !ruby/object:Gem::Dependency
55
- name: rmagick
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '4.3'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '4.3'
68
54
  - !ruby/object:Gem::Dependency
69
55
  name: rsvg2
70
56
  requirement: !ruby/object:Gem::Requirement
@@ -121,20 +107,6 @@ dependencies:
121
107
  - - ">="
122
108
  - !ruby/object:Gem::Version
123
109
  version: '0'
124
- - !ruby/object:Gem::Dependency
125
- name: yaml
126
- requirement: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - ">="
129
- - !ruby/object:Gem::Version
130
- version: '0'
131
- type: :development
132
- prerelease: false
133
- version_requirements: !ruby/object:Gem::Requirement
134
- requirements:
135
- - - ">="
136
- - !ruby/object:Gem::Version
137
- version: '0'
138
110
  description: Syntax tree generator made with Ruby
139
111
  email:
140
112
  - yohasebe@gmail.com