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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +211 -23
- data/README.md +57 -4
- data/bin/rsyntaxtree +10 -11
- data/lib/rsyntaxtree/base_graph.rb +17 -8
- data/lib/rsyntaxtree/element.rb +35 -3
- data/lib/rsyntaxtree/lsif_graph.rb +9 -3
- data/lib/rsyntaxtree/markup_parser.rb +16 -4
- data/lib/rsyntaxtree/notation_core.md +108 -42
- data/lib/rsyntaxtree/notation_examples.md +97 -5
- data/lib/rsyntaxtree/string_parser.rb +50 -1
- data/lib/rsyntaxtree/svg_graph.rb +271 -15
- data/lib/rsyntaxtree/tikz_generator.rb +9 -0
- data/lib/rsyntaxtree/version.rb +1 -1
- data/lib/rsyntaxtree.rb +93 -68
- data/rsyntaxtree.gemspec +4 -3
- metadata +1 -29
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
|
|
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
|
|
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
|
-
|
|
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
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
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 :
|
|
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, :
|
|
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
|
-
|
|
317
|
-
|
|
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
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
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
|
-
#
|
|
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:
|
|
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
|