rsyntaxtree 1.12.0 → 1.13.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 +19 -0
- data/bin/rsyntaxtree +5 -3
- data/lib/rsyntaxtree/base_graph.rb +151 -0
- data/lib/rsyntaxtree/element.rb +95 -15
- data/lib/rsyntaxtree/lsif_graph.rb +12 -0
- data/lib/rsyntaxtree/notation_examples.md +8 -57
- data/lib/rsyntaxtree/string_parser.rb +9 -4
- data/lib/rsyntaxtree/svg_graph.rb +102 -6
- data/lib/rsyntaxtree/tikz_generator.rb +16 -0
- data/lib/rsyntaxtree/utils.rb +27 -15
- data/lib/rsyntaxtree/version.rb +1 -1
- data/lib/rsyntaxtree.rb +38 -6
- data/rsyntaxtree.gemspec +11 -3
- metadata +1 -9
- data/.gitattributes +0 -10
- data/.github/FUNDING.yml +0 -6
- data/.github/workflows/test.yml +0 -35
- data/.gitignore +0 -25
- data/.ruby-version +0 -1
- data/Dockerfile +0 -41
- data/Gemfile +0 -15
- data/Rakefile +0 -111
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32cc6cec6f19c5fe62dc9a6de16493d6db35680f72e40b6bbd36f0b1e98add96
|
|
4
|
+
data.tar.gz: b6f928fd6919bb289765fac684ae3e98f7988103985deebcb6f73e7f8836e939
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50d8b1e91e3c71286406bf45e5e5cdeb2d8170f98a513a6662f0b593a7e077b7af2650057e6d9670469ed937b6ddc7c0073868df6833721c2ba3c81a8f691c46
|
|
7
|
+
data.tar.gz: 92df8d83ddbabcdd3cbeaa30f26868da4a13c168d5f97600475f552a197f7ebba70a80cc6ab272f98e2a68c1b8bd9d9a9094d3b01235ec556790f4a944337fee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.13.0] - 2026-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Derivations. A derivation puts the words first and the result last, and
|
|
7
|
+
joins what each step combines with one rule drawn across all of it rather
|
|
8
|
+
than with a line to each daughter. `derivation` draws the rules and
|
|
9
|
+
`direction: btt` turns the tree over; together they give the format
|
|
10
|
+
categorial grammar is written in, and `derivation` on its own marks the
|
|
11
|
+
spans of an ordinary tree. The name of each step rides in the label after a
|
|
12
|
+
column break and is set beside the end of its rule. A derivation runs down
|
|
13
|
+
the page and is drawn with its rules, so `direction: ltr` and hiding the
|
|
14
|
+
default connectors are refused rather than approximated.
|
|
15
|
+
- `direction: btt`, the layout turned over, leaves first.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Connectors follow the tree when it is turned over. They kept the ends they
|
|
19
|
+
have when the root is at the top, so every line ran from above the daughter,
|
|
20
|
+
through its label and the mother's, to below the mother.
|
|
21
|
+
|
|
3
22
|
## [1.12.0] - 2026-08
|
|
4
23
|
|
|
5
24
|
Everything needed to write this notation is now available as text, built from
|
data/bin/rsyntaxtree
CHANGED
|
@@ -32,7 +32,7 @@ CONFIG_VALIDATORS = {
|
|
|
32
32
|
tidy: ->(v) { OPTION_VALUES[:tidy].include?(v.to_s) ? nil : "must be off, symmetric, low, medium, or high" },
|
|
33
33
|
hspacing: ->(v) { v.is_a?(Numeric) && v >= 0.5 && v <= 3.0 ? nil : "must be in the range of 0.5-3.0" },
|
|
34
34
|
tidy_spacing: ->(v) { v.is_a?(Numeric) && v >= 0.5 && v <= 3.0 ? nil : "must be in the range of 0.5-3.0" },
|
|
35
|
-
direction: ->(v) { OPTION_VALUES[:direction].include?(v.to_s) ? nil : "must be
|
|
35
|
+
direction: ->(v) { OPTION_VALUES[:direction].include?(v.to_s) ? nil : "must be one of: #{OPTION_VALUES[:direction].join(', ')}" },
|
|
36
36
|
hyphen: ->(v) { OPTION_VALUES[:hyphen].include?(v.to_s) ? nil : "must be markup or literal" }
|
|
37
37
|
}.freeze
|
|
38
38
|
|
|
@@ -122,7 +122,8 @@ opts = Optimist.options do
|
|
|
122
122
|
opt :tidy, "tidy tree layout: off, symmetric (radical symmetrization), low (packed, strict leaf positions), medium (packed; leaves may overlap but never swap order), or high (packed; leaf order kept per row only)", default: "off", short: :none
|
|
123
123
|
opt :hspacing, "Horizontal spacing factor (counterpart of vheight): 0.5-3.0", default: 1.0, short: :none
|
|
124
124
|
opt :tidy_spacing, "DEPRECATED alias of --hspacing: 0.5-3.0", default: 1.0, short: :none
|
|
125
|
-
opt :direction, "Tree layout direction: ttb (top-to-bottom)
|
|
125
|
+
opt :direction, "Tree layout direction: ttb (top-to-bottom), ltr (left-to-right) or btt (bottom-to-top, the root last)", default: "ttb", short: :d
|
|
126
|
+
opt :derivation, "Join each node to its daughters with one rule drawn across them, as a derivation is written, rather than with a line to each: on, off", default: "off", short: :none
|
|
126
127
|
opt :hyphen, "How a hyphen in a label reads: markup (-underline-) or literal (a hyphen; \-underline\- then underlines)", default: "markup", short: :none
|
|
127
128
|
opt :validate, "Validate the input without drawing or writing a file: print a machine-readable diagnosis (JSON) to stdout and report the result in the exit code", short: :none
|
|
128
129
|
opt :notation, "Print the notation reference to stdout and exit", short: :none
|
|
@@ -131,7 +132,8 @@ opts = Optimist.options do
|
|
|
131
132
|
end
|
|
132
133
|
|
|
133
134
|
Optimist.die :outdir, "must be an exsting directory path" unless FileTest.directory?(opts[:outdir])
|
|
134
|
-
Optimist.die :direction, "must be
|
|
135
|
+
Optimist.die :direction, "must be one of: #{OPTION_VALUES[:direction].join(', ')}" unless OPTION_VALUES[:direction].include?(opts[:direction])
|
|
136
|
+
Optimist.die :derivation, "must be one of: #{OPTION_VALUES[:derivation].join(', ')}" unless OPTION_VALUES[:derivation].include?(opts[:derivation])
|
|
135
137
|
Optimist.die :hyphen, "must be markup or literal" unless OPTION_VALUES[:hyphen].include?(opts[:hyphen])
|
|
136
138
|
Optimist.die :format, "must be #{FORMAT_LIST_MESSAGE}" unless FORMATS.include?(opts[:format])
|
|
137
139
|
Optimist.die :leafstyle, "must be auto, triangle, bar, or nothing" unless OPTION_VALUES[:leafstyle].include?(opts[:leafstyle])
|
|
@@ -67,6 +67,7 @@ module RSyntaxTree
|
|
|
67
67
|
@fontset = params[:fontset]
|
|
68
68
|
@fontsize = params[:fontsize]
|
|
69
69
|
@mirror = params[:mirror] == true
|
|
70
|
+
@derivation = params[:derivation] == true
|
|
70
71
|
# tidy is one layout scale: "symmetric" (radical symmetrization) |
|
|
71
72
|
# "off" | "low" (packing, strict leaf positions) | "medium" (packing
|
|
72
73
|
# with cross-row tucking as long as no two leaves swap left-right
|
|
@@ -182,6 +183,17 @@ module RSyntaxTree
|
|
|
182
183
|
accum_array.sum
|
|
183
184
|
end
|
|
184
185
|
|
|
186
|
+
# The name of the step hangs past the right end of its rule, and the
|
|
187
|
+
# rule reaches the full width of what the step combines. With no room
|
|
188
|
+
# kept for it the name lands on the next subtree's rule and two steps
|
|
189
|
+
# read as one, so the node claims that much more width. The label is
|
|
190
|
+
# drawn from the left edge and does not move.
|
|
191
|
+
#
|
|
192
|
+
# Doubled: the children sit centred in the width their mother claims,
|
|
193
|
+
# so half of whatever is added lands on the left where nothing needs
|
|
194
|
+
# it, and only the other half reaches the side the name is on.
|
|
195
|
+
accum_width += rule_name_room(target) * 2
|
|
196
|
+
|
|
185
197
|
if target.content_width > accum_width
|
|
186
198
|
# Parent label is wider than children's total width.
|
|
187
199
|
# Distribute the excess equally among children to prevent
|
|
@@ -234,9 +246,91 @@ module RSyntaxTree
|
|
|
234
246
|
end
|
|
235
247
|
target.height = accum_array.max - target.vertical_indent
|
|
236
248
|
accum_array.max
|
|
249
|
+
end.tap { level_derivation_rows if id == 1 && @derivation }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# In a derivation every premise is given at the start, so the words stand in
|
|
253
|
+
# one row and each step sits below everything it draws on. A node's row is
|
|
254
|
+
# therefore its distance from the words rather than its distance from the
|
|
255
|
+
# root: a step joining a category to a derivation three deep goes below
|
|
256
|
+
# both. Placed by depth from the root instead, the words come out in a
|
|
257
|
+
# staircase, each at the depth of its own branch.
|
|
258
|
+
#
|
|
259
|
+
# Run after the ordinary placement, so the row heights and the drop between
|
|
260
|
+
# them are the ones that pass settled on.
|
|
261
|
+
# Levelled twice, and the two are not the same pass. The first runs inside
|
|
262
|
+
# the layout, because tidy packs subtrees by their contours and a contour is
|
|
263
|
+
# read off the vertical positions: rows it has not seen levelled pack as
|
|
264
|
+
# though the figure were a tree, and two rules of one row come out
|
|
265
|
+
# overlapping. The second runs after the layout has been turned over, since
|
|
266
|
+
# turning it over sets each box against its own bottom edge, which leaves a
|
|
267
|
+
# tall label — a feature matrix, say — standing proud of its row. Counting
|
|
268
|
+
# rows from the end the words are at is what makes the second pass the same
|
|
269
|
+
# reckoning as the first, read the other way up.
|
|
270
|
+
def level_derivation_rows(flipped: false)
|
|
271
|
+
root_height = subtree_height(1)
|
|
272
|
+
rows = {}
|
|
273
|
+
tallest = Hash.new(0.0)
|
|
274
|
+
@element_list.get_elements.each do |e|
|
|
275
|
+
height = subtree_height(e.id)
|
|
276
|
+
row = flipped ? height : root_height - height
|
|
277
|
+
rows[e.id] = row
|
|
278
|
+
tallest[row] = [tallest[row], e.content_height].max
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
drop = @global[:height_connector].to_f
|
|
282
|
+
top = {}
|
|
283
|
+
y = 0.0
|
|
284
|
+
tallest.keys.sort.each do |row|
|
|
285
|
+
top[row] = y
|
|
286
|
+
y += tallest[row] + drop
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
@element_list.get_elements.each { |e| e.vertical_indent = top[rows[e.id]] }
|
|
290
|
+
|
|
291
|
+
# Which row each element ended up in, kept for the drawing: a rule belongs
|
|
292
|
+
# between two rows, not between two labels. Placed from the label instead,
|
|
293
|
+
# a step whose result is taller than its neighbours — a feature matrix
|
|
294
|
+
# among plain categories — had its rule drawn at a height of its own while
|
|
295
|
+
# the rules beside it stayed on the row.
|
|
296
|
+
@derivation_rows = rows
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# How far a row reaches, read from the labels as they stand. Taken when the
|
|
300
|
+
# rows were levelled it would be the measured height, and the drawing works
|
|
301
|
+
# to a slightly different one.
|
|
302
|
+
def derivation_row_extent(row)
|
|
303
|
+
@derivation_extents ||= {}
|
|
304
|
+
@derivation_extents[row] ||= begin
|
|
305
|
+
members = @element_list.get_elements.select { |e| @derivation_rows[e.id] == row }
|
|
306
|
+
[members.map(&:vertical_indent).min,
|
|
307
|
+
members.map { |e| e.vertical_indent + e.content_height }.max]
|
|
237
308
|
end
|
|
238
309
|
end
|
|
239
310
|
|
|
311
|
+
# The rule for a step runs between the row its result sits in and the row
|
|
312
|
+
# holding the last of its premises — always the next row along, since a step
|
|
313
|
+
# is one taller than the tallest thing it draws on.
|
|
314
|
+
def derivation_rule_band(parent)
|
|
315
|
+
return nil unless @derivation_rows
|
|
316
|
+
|
|
317
|
+
row = @derivation_rows[parent.id]
|
|
318
|
+
neighbour = row + (@direction == "btt" ? -1 : 1)
|
|
319
|
+
return nil unless @derivation_rows.value?(neighbour)
|
|
320
|
+
|
|
321
|
+
here = derivation_row_extent(row)
|
|
322
|
+
there = derivation_row_extent(neighbour)
|
|
323
|
+
@direction == "btt" ? [there[1], here[0]] : [here[1], there[0]]
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Steps from +id+ down to the furthest word under it. A word is zero.
|
|
327
|
+
def subtree_height(id)
|
|
328
|
+
node = @element_list.get_id(id)
|
|
329
|
+
return 0 if node.children.empty?
|
|
330
|
+
|
|
331
|
+
1 + node.children.map { |c| subtree_height(c) }.max
|
|
332
|
+
end
|
|
333
|
+
|
|
240
334
|
def make_balance(id = 1)
|
|
241
335
|
target = @element_list.get_id(id)
|
|
242
336
|
if target.children.empty?
|
|
@@ -305,6 +399,15 @@ module RSyntaxTree
|
|
|
305
399
|
parent = @element_list.get_id(id)
|
|
306
400
|
children = parent.children.map { |c| @element_list.get_id(c) }
|
|
307
401
|
|
|
402
|
+
# A derivation joins its premises with one rule drawn across all of them,
|
|
403
|
+
# not with a line to each. The rule replaces the lines, so a tree is drawn
|
|
404
|
+
# this way throughout or not at all.
|
|
405
|
+
if @derivation && !children.empty?
|
|
406
|
+
rule_to_parent(parent, children)
|
|
407
|
+
parent.children.each { |c| draw_connector(c) }
|
|
408
|
+
return
|
|
409
|
+
end
|
|
410
|
+
|
|
308
411
|
if children.size == 1
|
|
309
412
|
child = children[0]
|
|
310
413
|
case @leafstyle
|
|
@@ -345,6 +448,20 @@ module RSyntaxTree
|
|
|
345
448
|
(children_indent << target_indent).min
|
|
346
449
|
end
|
|
347
450
|
|
|
451
|
+
# Width to keep clear to the right of a step's rule for the name beside it,
|
|
452
|
+
# with a gap between the name and whatever follows. Zero unless the tree is
|
|
453
|
+
# drawn as a derivation and the step has a name.
|
|
454
|
+
def rule_name_room(element)
|
|
455
|
+
return 0 unless @derivation
|
|
456
|
+
|
|
457
|
+
name = element.rule_name
|
|
458
|
+
return 0 if name.nil? || name.empty?
|
|
459
|
+
|
|
460
|
+
size = (@fontsize * 0.8).round(2)
|
|
461
|
+
width = FontMetrics.get_metrics(name, @fontset[:family], size, :normal, :normal).width
|
|
462
|
+
width + @global[:width_half_x]
|
|
463
|
+
end
|
|
464
|
+
|
|
348
465
|
def get_rightmost(id = 1)
|
|
349
466
|
target = @element_list.get_id(id)
|
|
350
467
|
target_right_end = target.horizontal_indent + target.content_width
|
|
@@ -363,6 +480,19 @@ module RSyntaxTree
|
|
|
363
480
|
end
|
|
364
481
|
end
|
|
365
482
|
|
|
483
|
+
# Flip the laid-out tree vertically: the root ends up at the bottom and the
|
|
484
|
+
# leaves at the top, which is how a derivation is written — the words at the
|
|
485
|
+
# top, each step below the material it combines. Done after the layout is
|
|
486
|
+
# final, like the horizontal flip, so connectors, triangles, paths and
|
|
487
|
+
# region shades follow from the coordinates without knowing about it.
|
|
488
|
+
def flip_vertical
|
|
489
|
+
elements = @element_list.get_elements
|
|
490
|
+
max_bottom = elements.map { |e| e.vertical_indent + e.content_height }.max
|
|
491
|
+
elements.each do |e|
|
|
492
|
+
e.vertical_indent = max_bottom - (e.vertical_indent + e.content_height)
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
366
496
|
# Flip the laid-out tree horizontally (RTL linguistics convention: the
|
|
367
497
|
# first word sits at the right edge). Connectors, triangles, polylines,
|
|
368
498
|
# movement paths, and region shades all derive from element coordinates,
|
|
@@ -425,9 +555,23 @@ module RSyntaxTree
|
|
|
425
555
|
x0 -= ext
|
|
426
556
|
x1 += ext
|
|
427
557
|
end
|
|
558
|
+
# A step's name is drawn past the right end of its rule, and the rule
|
|
559
|
+
# reaches the full width of what the step combines. Packing sees only the
|
|
560
|
+
# labels, so without this the neighbour is pulled up against the name and
|
|
561
|
+
# two steps read as one long rule.
|
|
562
|
+
x1 = [x1, subtree_right(node.id) + rule_name_room(node)].max if @derivation
|
|
428
563
|
[node.vertical_indent, node.vertical_indent + node.content_height, x0, x1]
|
|
429
564
|
end
|
|
430
565
|
|
|
566
|
+
# Right edge of the subtree rooted at +id+, from wherever the layout has
|
|
567
|
+
# put things so far.
|
|
568
|
+
def subtree_right(id)
|
|
569
|
+
node = @element_list.get_id(id)
|
|
570
|
+
right = node.horizontal_indent + node.content_width
|
|
571
|
+
node.children.each { |c| right = [right, subtree_right(c)].max }
|
|
572
|
+
right
|
|
573
|
+
end
|
|
574
|
+
|
|
431
575
|
# Effective-rect list of the subtree rooted at +id+, as [y0, y1, x0, x1].
|
|
432
576
|
# A region-shaded (%) node additionally contributes the shade's padded
|
|
433
577
|
# bounding rect so that neighboring subtrees keep clear of the plane.
|
|
@@ -900,6 +1044,10 @@ module RSyntaxTree
|
|
|
900
1044
|
finalize_ltr if @direction == "ltr"
|
|
901
1045
|
|
|
902
1046
|
# RTL flip (mirror option): after the layout is final, before drawing
|
|
1047
|
+
# btt is the top-to-bottom layout turned over, so the layout runs as
|
|
1048
|
+
# usual and the flip comes after it.
|
|
1049
|
+
flip_vertical if @direction == "btt"
|
|
1050
|
+
level_derivation_rows(flipped: true) if @derivation && @direction == "btt"
|
|
903
1051
|
mirror_layout if @mirror
|
|
904
1052
|
|
|
905
1053
|
draw_elements
|
|
@@ -915,6 +1063,9 @@ module RSyntaxTree
|
|
|
915
1063
|
max_x = r if r > max_x
|
|
916
1064
|
max_y = b if b > max_y
|
|
917
1065
|
end
|
|
1066
|
+
# A rule's name is set beside the rule, past the right edge of every box
|
|
1067
|
+
# the loop above measured.
|
|
1068
|
+
max_x = @rule_name_edge if @rule_name_edge.to_f > max_x
|
|
918
1069
|
width = max_x + @global[:h_gap_between_nodes]
|
|
919
1070
|
height = max_y
|
|
920
1071
|
height = @height if @height > height
|
data/lib/rsyntaxtree/element.rb
CHANGED
|
@@ -13,9 +13,14 @@ require_relative "color_names"
|
|
|
13
13
|
|
|
14
14
|
module RSyntaxTree
|
|
15
15
|
class Element
|
|
16
|
-
attr_accessor :id, :parent, :type, :level, :width, :height, :content, :content_width, :text_width, :content_height, :horizontal_indent, :vertical_indent, :triangle, :enclosure, :children, :font, :fontsize, :contains_phrase, :path, :color, :raw_content, :region, :region_color
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
attr_accessor :rule_name, :id, :parent, :type, :level, :width, :height, :content, :content_width, :text_width, :content_height, :horizontal_indent, :vertical_indent, :triangle, :enclosure, :children, :font, :fontsize, :contains_phrase, :path, :color, :raw_content, :region, :region_color
|
|
17
|
+
|
|
18
|
+
# names_a_rule says the content is a mother's label rather than a leaf's
|
|
19
|
+
# text. Only a mother has a step under it for a name to sit beside, and a
|
|
20
|
+
# leaf's `a\tb` is a row of two columns, so the name is read out of the one
|
|
21
|
+
# and left alone in the other.
|
|
22
|
+
def initialize(id, parent, content, level, fontset, fontsize, global,
|
|
23
|
+
names_a_rule = false)
|
|
19
24
|
@global = global
|
|
20
25
|
@type = ETYPE_LEAF
|
|
21
26
|
@id = id # Unique element id
|
|
@@ -36,6 +41,20 @@ module RSyntaxTree
|
|
|
36
41
|
|
|
37
42
|
@fontset = fontset
|
|
38
43
|
@fontsize = fontsize
|
|
44
|
+
# In a derivation the label may carry the name of the rule that produced
|
|
45
|
+
# it, written after a column break: `S/NP\t\>B`. The name belongs beside
|
|
46
|
+
# the rule rather than beside the result, so it comes out of the label
|
|
47
|
+
# here, before the label is measured, and BaseGraph draws it at the end
|
|
48
|
+
# of the rule.
|
|
49
|
+
if global && global[:derivation] && names_a_rule
|
|
50
|
+
at = rule_name_break(content)
|
|
51
|
+
name = at && content[(at + 2)..].to_s.strip
|
|
52
|
+
if name && !name.empty?
|
|
53
|
+
@rule_name = name
|
|
54
|
+
content = content[0...at]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
39
58
|
@raw_content = content.sub(/\^?(?:\+-?>?<?\d+)+\^?\z/, '')
|
|
40
59
|
|
|
41
60
|
parsed = Markup.parse(prepare_markup(content))
|
|
@@ -65,12 +84,21 @@ module RSyntaxTree
|
|
|
65
84
|
# enclosure. Such nodes act as pass-through joints: connectors run
|
|
66
85
|
# continuously through them, which lets a `<>` chain push a leaf down
|
|
67
86
|
# to align with deeper leaves while the line stays unbroken.
|
|
87
|
+
#
|
|
88
|
+
# A label that is one whole feature matrix keeps its text inside the
|
|
89
|
+
# matrix and leaves none of it here, and the matrix is a decoration on the
|
|
90
|
+
# run rather than the element's own enclosure. Counted as empty, such a
|
|
91
|
+
# node was joined as a pass-through joint and the connectors were run to
|
|
92
|
+
# the middle of it — through the matrix and the rows written in it.
|
|
68
93
|
def empty_label?
|
|
69
94
|
return false if @enclosure && @enclosure != :none
|
|
70
95
|
|
|
71
96
|
@content.all? do |c|
|
|
72
97
|
c[:type] == :text &&
|
|
73
|
-
c[:elements].all?
|
|
98
|
+
c[:elements].all? do |e|
|
|
99
|
+
!e[:decoration].to_a.include?(:matrix) &&
|
|
100
|
+
e[:text].gsub(WHITESPACE_BLOCK, "").strip.empty?
|
|
101
|
+
end
|
|
74
102
|
end
|
|
75
103
|
end
|
|
76
104
|
|
|
@@ -146,6 +174,16 @@ module RSyntaxTree
|
|
|
146
174
|
[:unclosed_matrix,
|
|
147
175
|
->(s) { s.scan("#(").size > s.scan("#)").size ? s + "#)" : s },
|
|
148
176
|
"A matrix opened with '#(' is never closed with '#)'."],
|
|
177
|
+
# A derivation writes the name of each step after a column break, and
|
|
178
|
+
# the combinators it is written with are made of the same characters as
|
|
179
|
+
# the whitespace marker. Read as an ordinary tree such a label is
|
|
180
|
+
# unclosed markup, and the advice below is about spaces — the opposite of
|
|
181
|
+
# what the writer needs. Asked here, before those, so that a derivation
|
|
182
|
+
# written without the option on is told so.
|
|
183
|
+
[:rule_name_without_derivation,
|
|
184
|
+
->(s) { s.include?('\\n') ? s : s.sub(/(?<!\\)\\t.*\z/m, "") },
|
|
185
|
+
"This label names a rule after a column break, the way a derivation " \
|
|
186
|
+
"does. Turn derivation on to draw it as one, or escape the break as \\\\t."],
|
|
149
187
|
# Neutralising every occurrence of one character, rather than adding a
|
|
150
188
|
# closing one, locates the culprit wherever it sits in the label — an
|
|
151
189
|
# opener left unclosed halfway down a matrix is not fixed by appending.
|
|
@@ -282,7 +320,8 @@ module RSyntaxTree
|
|
|
282
320
|
# the padding and the second is what actually separates the
|
|
283
321
|
# bracket from the descenders of the line above it.
|
|
284
322
|
content[:top_room] = matrix_vertical_room * 2
|
|
285
|
-
elements_height << e[:height]
|
|
323
|
+
elements_height << with_first_row_margin(e[:height], one_bvm_given)
|
|
324
|
+
one_bvm_given = true
|
|
286
325
|
row_width += e[:width]
|
|
287
326
|
next
|
|
288
327
|
end
|
|
@@ -313,7 +352,12 @@ module RSyntaxTree
|
|
|
313
352
|
standard_metrics = FontMetrics.get_metrics('X', font, fontsize, :normal, :normal)
|
|
314
353
|
|
|
315
354
|
height = standard_metrics.height
|
|
316
|
-
|
|
355
|
+
# The rhythm a row advances by belongs to the label, not to the size
|
|
356
|
+
# a run inside it happens to be set at. Taken from the run, a label
|
|
357
|
+
# that is nothing but a subscript measured a subscript tall and came
|
|
358
|
+
# out shorter than the text drawn in it, so its daughters were
|
|
359
|
+
# placed up into it.
|
|
360
|
+
line_height = @global[:single_x_metrics].height
|
|
317
361
|
if /\A[<>]+\z/ =~ text
|
|
318
362
|
width = standard_metrics.width * text.size / 2
|
|
319
363
|
elsif text.contains_emoji?
|
|
@@ -356,7 +400,7 @@ module RSyntaxTree
|
|
|
356
400
|
# ENCLOSURE_SIZE only for content that will not fit.
|
|
357
401
|
band = FontMetrics.get_metrics("Xg", font, fontsize, :normal, :normal)
|
|
358
402
|
descender = band.ink_height - band.ink_above
|
|
359
|
-
centre =
|
|
403
|
+
centre = FontMetrics.visual_centre(font, fontsize)
|
|
360
404
|
|
|
361
405
|
ink = FontMetrics.get_metrics(text, font, fontsize, style, weight)
|
|
362
406
|
ink_height = ink.ink_height.to_f
|
|
@@ -399,14 +443,8 @@ module RSyntaxTree
|
|
|
399
443
|
# the height of the nodes above it, so a node measured short of the
|
|
400
444
|
# rhythm pulls its own children up and off the row its cousins sit
|
|
401
445
|
# on.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if one_bvm_given
|
|
405
|
-
elements_height << measured
|
|
406
|
-
else
|
|
407
|
-
one_bvm_given = true
|
|
408
|
-
elements_height << measured + @global[:box_vertical_margin]
|
|
409
|
-
end
|
|
446
|
+
elements_height << with_first_row_margin([height, line_height].max, one_bvm_given)
|
|
447
|
+
one_bvm_given = true
|
|
410
448
|
|
|
411
449
|
e[:width] = width
|
|
412
450
|
row_width += width
|
|
@@ -431,6 +469,48 @@ module RSyntaxTree
|
|
|
431
469
|
@global[:width_half_x] * MATRIX_BRACKET_ROOM
|
|
432
470
|
end
|
|
433
471
|
|
|
472
|
+
# Where the break that names the rule is, or nil if the label does not name
|
|
473
|
+
# one. It is the break at the top level of the label: a feature matrix is
|
|
474
|
+
# written with breaks of its own and those belong to its columns, so a step
|
|
475
|
+
# whose category is a matrix can still name the rule that produced it.
|
|
476
|
+
#
|
|
477
|
+
# Exactly one, and no line break outside a matrix: a rule has one name, and
|
|
478
|
+
# a label written over several lines is a matrix rather than a step. A
|
|
479
|
+
# break the writer escaped is theirs and is not a break here.
|
|
480
|
+
def rule_name_break(content)
|
|
481
|
+
depth = 0
|
|
482
|
+
breaks = []
|
|
483
|
+
lined = false
|
|
484
|
+
index = 0
|
|
485
|
+
while index < content.length
|
|
486
|
+
case content[index, 2]
|
|
487
|
+
when '\\t' then breaks << index if depth.zero?
|
|
488
|
+
when '\\n' then lined = true if depth.zero?
|
|
489
|
+
when "#(" then depth += 1
|
|
490
|
+
when "#)" then depth -= 1
|
|
491
|
+
else
|
|
492
|
+
# A backslash takes the character after it, whatever that character
|
|
493
|
+
# is. Only the two breaks above are read here; every other escape is
|
|
494
|
+
# the notation's, and `\#` is a hash that opens no matrix — counted as
|
|
495
|
+
# one it left the depth wrong and the break that names the rule was
|
|
496
|
+
# looked for in the wrong place.
|
|
497
|
+
index += content[index] == "\\" ? 2 : 1
|
|
498
|
+
next
|
|
499
|
+
end
|
|
500
|
+
index += 2
|
|
501
|
+
end
|
|
502
|
+
breaks.first if !lined && breaks.size == 1
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# The first row of a label carries a margin above it that holds the text
|
|
506
|
+
# clear of the connector coming down to the node. Every row is entered
|
|
507
|
+
# through here so that the one holding a feature matrix is given it too: it
|
|
508
|
+
# used to be measured without, and a matrix node came out shorter than the
|
|
509
|
+
# rows drawn in it, which sat its daughters up inside the matrix.
|
|
510
|
+
def with_first_row_margin(height, already_given)
|
|
511
|
+
already_given ? height : height + @global[:box_vertical_margin]
|
|
512
|
+
end
|
|
513
|
+
|
|
434
514
|
# Vertical room a nested matrix keeps between itself and the rows above and
|
|
435
515
|
# below it.
|
|
436
516
|
def matrix_vertical_room
|
|
@@ -69,6 +69,17 @@ module RSyntaxTree
|
|
|
69
69
|
parent = @element_list.get_id(id)
|
|
70
70
|
children = parent.children.map { |c| @element_list.get_id(c) }
|
|
71
71
|
|
|
72
|
+
# A derivation joins a node to its daughters with one rule across all of
|
|
73
|
+
# them. Recorded as what it is: this format states that it carries how
|
|
74
|
+
# the figure was drawn, so calling these lines would be a false record.
|
|
75
|
+
if @derivation && !children.empty?
|
|
76
|
+
children.each do |child|
|
|
77
|
+
@edges << { from: parent.id, to: child.id, type: "dominance", connector: "rule" }
|
|
78
|
+
end
|
|
79
|
+
parent.children.each { |c| draw_connector(c) }
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
72
83
|
if children.size == 1
|
|
73
84
|
child = children[0]
|
|
74
85
|
case @leafstyle
|
|
@@ -275,6 +286,7 @@ module RSyntaxTree
|
|
|
275
286
|
font_size: (@params[:fontsize] / FONT_SCALING).round,
|
|
276
287
|
color: @params[:color],
|
|
277
288
|
connector: @params[:leafstyle],
|
|
289
|
+
derivation: @params[:derivation] == true,
|
|
278
290
|
connector_height: @params[:vheight],
|
|
279
291
|
horizontal_spacing: @params[:hspacing] || 1.0,
|
|
280
292
|
line_width: @params[:linewidth],
|
|
@@ -276,73 +276,24 @@ Source: Radford 2004
|
|
|
276
276
|
]
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
## 011 — CCG
|
|
279
|
+
## 011 — CCG derivation: forward and backward application
|
|
280
280
|
|
|
281
281
|
Category: Combinatory Categorial Grammar
|
|
282
|
-
Settings: color=none fontstyle=noto-serif leafstyle=nothing tidy=low
|
|
282
|
+
Settings: color=none derivation=on direction=btt fontstyle=noto-serif leafstyle=nothing tidy=low vheight=0.7
|
|
283
|
+
Source: Steedman 2000
|
|
283
284
|
|
|
284
285
|
```
|
|
285
|
-
[S\
|
|
286
|
-
\<
|
|
287
|
-
[NP\
|
|
288
|
-
\>
|
|
289
|
-
[NP/N\
|
|
290
|
-
---\
|
|
291
|
-
*the*
|
|
292
|
-
]
|
|
293
|
-
[N\
|
|
294
|
-
---\
|
|
295
|
-
*dog*
|
|
296
|
-
]
|
|
297
|
-
]
|
|
298
|
-
[S\\NP\
|
|
299
|
-
\>
|
|
300
|
-
[(S\\NP)\\NP\
|
|
301
|
-
---\
|
|
302
|
-
*bit*
|
|
303
|
-
]
|
|
304
|
-
[NP\
|
|
305
|
-
---\
|
|
306
|
-
*John*
|
|
307
|
-
]
|
|
308
|
-
]
|
|
309
|
-
]
|
|
286
|
+
[S\t< [NP\t> [NP/N the] [N dog]] [S\\NP\t> [(S\\NP)/NP bit] [NP John]]]
|
|
310
287
|
```
|
|
311
288
|
|
|
312
|
-
## 012 — CCG
|
|
289
|
+
## 012 — CCG derivation: a relative clause
|
|
313
290
|
|
|
314
291
|
Category: Combinatory Categorial Grammar
|
|
315
|
-
Settings: color=none fontstyle=noto-serif leafstyle=nothing tidy=low vheight=
|
|
292
|
+
Settings: color=none derivation=on direction=btt fontstyle=noto-serif leafstyle=nothing tidy=low vheight=0.7
|
|
293
|
+
Source: Steedman 2000
|
|
316
294
|
|
|
317
295
|
```
|
|
318
|
-
[S\
|
|
319
|
-
\>
|
|
320
|
-
[S/NP\
|
|
321
|
-
B_\>_
|
|
322
|
-
[S/(S\\NP)\
|
|
323
|
-
T_\>_
|
|
324
|
-
[NP\
|
|
325
|
-
\>
|
|
326
|
-
[NP/N\
|
|
327
|
-
---\
|
|
328
|
-
*the*
|
|
329
|
-
]
|
|
330
|
-
[N\
|
|
331
|
-
---\
|
|
332
|
-
*dog*
|
|
333
|
-
]
|
|
334
|
-
]
|
|
335
|
-
]
|
|
336
|
-
[(S\\NP)/NP\
|
|
337
|
-
---\
|
|
338
|
-
*bit*
|
|
339
|
-
]
|
|
340
|
-
]
|
|
341
|
-
[NP\
|
|
342
|
-
---\
|
|
343
|
-
*John*
|
|
344
|
-
]
|
|
345
|
-
]
|
|
296
|
+
[NP\t> [NP/N the] [N\t< [N man] [N\\N\t> [(N\\N)/(S/NP) that] [S/NP\t>B [S/(S\\NP)\t>T [NP Mary]] [(S\\NP)/NP saw]]]]]
|
|
346
297
|
```
|
|
347
298
|
|
|
348
299
|
## 013 — HPSG sample
|
|
@@ -41,7 +41,6 @@ module RSyntaxTree
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
@data = string # Store it for later...
|
|
44
|
-
fontset[:family] = fontset[:family_cjk] if @data.contains_cjk?
|
|
45
44
|
@elist = ElementList.new # Initialize internal element list
|
|
46
45
|
@pos = 0 # Position in the sentence
|
|
47
46
|
@id = 1 # ID for the next element
|
|
@@ -149,7 +148,13 @@ module RSyntaxTree
|
|
|
149
148
|
else
|
|
150
149
|
token += ch
|
|
151
150
|
end
|
|
152
|
-
|
|
151
|
+
# The characters a backslash may take, which is the grammar's own list
|
|
152
|
+
# (Markup's `escaped` rule) plus the two letters that name a break. A
|
|
153
|
+
# backslash before anything else is dropped here, and '#' was missing:
|
|
154
|
+
# `\#` arrived at the grammar as a bare '#', which opens an enclosure,
|
|
155
|
+
# so a label written with a hash in it lost the hash and everything
|
|
156
|
+
# after it went inside brackets instead.
|
|
157
|
+
when /[nt{}<>^+*_=~|%\-#]/
|
|
153
158
|
if escape
|
|
154
159
|
token += '\\' + ch
|
|
155
160
|
escape = false
|
|
@@ -202,7 +207,7 @@ module RSyntaxTree
|
|
|
202
207
|
parts[1] = token_r[spaceat, tl - spaceat].join
|
|
203
208
|
|
|
204
209
|
element = begin
|
|
205
|
-
Element.new(@id, parent, parts[0], @level, @fontset, @fontsize, @global)
|
|
210
|
+
Element.new(@id, parent, parts[0], @level, @fontset, @fontsize, @global, true)
|
|
206
211
|
rescue RSTError => e
|
|
207
212
|
# The first raw space splits a token into the node's label
|
|
208
213
|
# and its children (that split is the notation's core rule,
|
|
@@ -227,7 +232,7 @@ module RSyntaxTree
|
|
|
227
232
|
@id += 1
|
|
228
233
|
else
|
|
229
234
|
joined = token_r.join
|
|
230
|
-
element = Element.new(@id, parent, joined, @level, @fontset, @fontsize, @global)
|
|
235
|
+
element = Element.new(@id, parent, joined, @level, @fontset, @fontsize, @global, true)
|
|
231
236
|
@id += 1
|
|
232
237
|
newparent = element.id
|
|
233
238
|
end
|
|
@@ -469,7 +469,7 @@ module RSyntaxTree
|
|
|
469
469
|
style += "font-style: italic; " if e[:decoration].include?(:italic) || e[:decoration].include?(:bolditalic)
|
|
470
470
|
style += "\""
|
|
471
471
|
|
|
472
|
-
fontstyle = FontFamily.for_svg(@fontstyle
|
|
472
|
+
fontstyle = FontFamily.for_svg(@fontstyle)
|
|
473
473
|
|
|
474
474
|
if e[:decoration].include?(:box) || e[:decoration].include?(:circle) || e[:decoration].include?(:bar)
|
|
475
475
|
# Measured around the enclosed marks in Element#setup; the line
|
|
@@ -878,6 +878,105 @@ module RSyntaxTree
|
|
|
878
878
|
# (see Element#empty_label?) is a pass-through joint: the line runs to
|
|
879
879
|
# its center instead of stopping at its (invisible) text box, so the
|
|
880
880
|
# segments on both sides connect without a gap.
|
|
881
|
+
# One rule across all the premises, in place of a line to each. The rule
|
|
882
|
+
# runs the width of what it combines — the span from the leftmost child's
|
|
883
|
+
# left edge to the rightmost child's right edge — because that span is what
|
|
884
|
+
# the step applies to, and reading it off the children is what keeps it
|
|
885
|
+
# right when the layout moves.
|
|
886
|
+
#
|
|
887
|
+
# Drawn at the parent's edge facing the children, so it sits between the
|
|
888
|
+
# material above it and the result below in a derivation, and between a
|
|
889
|
+
# node and its daughters in an ordinary tree.
|
|
890
|
+
def rule_to_parent(parent, children)
|
|
891
|
+
return if children.empty?
|
|
892
|
+
|
|
893
|
+
# The whole of what the step combines, not the labels of its premises:
|
|
894
|
+
# a premise stands over a derivation of its own, and the rule is drawn
|
|
895
|
+
# across all of it. Taken from the subtree extents, which is what the
|
|
896
|
+
# layout already computes for the same purpose.
|
|
897
|
+
left = children.map { |c| get_leftmost(c.id) }.min
|
|
898
|
+
right = children.map { |c| get_rightmost(c.id) }.max
|
|
899
|
+
# The parent may be wider than everything it combines, which happens when
|
|
900
|
+
# a category is longer than the words under it. The rule covers both, so
|
|
901
|
+
# that the label it belongs to is never wider than its own rule.
|
|
902
|
+
left = [left, parent.horizontal_indent].min
|
|
903
|
+
right = [right, parent.horizontal_indent + parent.content_width].max
|
|
904
|
+
|
|
905
|
+
hctt = @global[:height_connector_to_text]
|
|
906
|
+
above, below = derivation_rule_band(parent)
|
|
907
|
+
y = if above
|
|
908
|
+
(above + hctt + below) / 2.0
|
|
909
|
+
else
|
|
910
|
+
child_edge = children.map { |c| c.vertical_indent + c.content_height }.max
|
|
911
|
+
parent_top = parent.vertical_indent
|
|
912
|
+
if parent_top >= child_edge
|
|
913
|
+
# The parent sits below: the rule goes between the two.
|
|
914
|
+
(child_edge + hctt + parent_top) / 2.0
|
|
915
|
+
else
|
|
916
|
+
# The parent sits above (an ordinary top-to-bottom tree).
|
|
917
|
+
(parent.vertical_indent + parent.content_height + hctt +
|
|
918
|
+
children.map(&:vertical_indent).min) / 2.0
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
@tree_data += @line_styles.sub(/X1/, left.to_s).sub(/Y1/, y.to_s)
|
|
923
|
+
.sub(/X2/, right.to_s).sub(/Y2/, y.to_s)
|
|
924
|
+
|
|
925
|
+
draw_rule_name(parent, right, y)
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
# The name of the rule, set beside the end of its own line and smaller than
|
|
929
|
+
# the categories, which is where a derivation puts it and how it keeps the
|
|
930
|
+
# name from reading as part of either row.
|
|
931
|
+
def draw_rule_name(parent, right, y)
|
|
932
|
+
name = parent.rule_name
|
|
933
|
+
return if name.nil? || name.empty?
|
|
934
|
+
|
|
935
|
+
size = (@fontsize * 0.8).round(2)
|
|
936
|
+
# Level with the rule, by the same reckoning an enclosure uses to sit
|
|
937
|
+
# level with what it encloses.
|
|
938
|
+
baseline = y + FontMetrics.visual_centre(@fontset[:family], size)
|
|
939
|
+
x = right + @global[:width_half_x]
|
|
940
|
+
# The name is the one thing drawn outside every element's own box, so the
|
|
941
|
+
# canvas has to be told where it ends or the last step's name is cut off
|
|
942
|
+
# at the edge of the image.
|
|
943
|
+
edge = x + FontMetrics.get_metrics(name, @fontset[:family], size, :normal, :normal).width
|
|
944
|
+
@rule_name_edge = edge if edge > @rule_name_edge.to_f
|
|
945
|
+
@tree_data += @text_styles.sub(/COLOR/, @col_line)
|
|
946
|
+
.sub(/fontsize/, size.to_s + "px;")
|
|
947
|
+
.sub(/X_VALUE/, x.to_s)
|
|
948
|
+
.sub(/Y_VALUE/, baseline.round(2).to_s)
|
|
949
|
+
.sub(/CONTENT/) { CGI.escapeHTML(name) }
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
# Where a connector meets each box in a vertical layout: the underside of
|
|
953
|
+
# whichever sits higher and the top of whichever sits lower. Turned over,
|
|
954
|
+
# the parent is the lower of the two, and keeping the top-to-bottom edges
|
|
955
|
+
# sent the line back up through both labels. An empty label has no text to
|
|
956
|
+
# clear, so the line runs to its middle.
|
|
957
|
+
def connector_edges(child, parent)
|
|
958
|
+
hctt = @global[:height_connector_to_text]
|
|
959
|
+
child_above = child.vertical_indent <= parent.vertical_indent
|
|
960
|
+
|
|
961
|
+
child_y = if child.empty_label?
|
|
962
|
+
child.vertical_indent + child.content_height / 2
|
|
963
|
+
elsif child_above
|
|
964
|
+
child.vertical_indent + child.content_height + hctt
|
|
965
|
+
else
|
|
966
|
+
child.vertical_indent + hctt / 2
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
parent_y = if parent.empty_label?
|
|
970
|
+
parent.vertical_indent + parent.content_height / 2
|
|
971
|
+
elsif child_above
|
|
972
|
+
parent.vertical_indent + hctt / 2
|
|
973
|
+
else
|
|
974
|
+
parent.vertical_indent + parent.content_height + hctt
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
[child_y, parent_y]
|
|
978
|
+
end
|
|
979
|
+
|
|
881
980
|
def line_to_parent(parent, child)
|
|
882
981
|
return if child.horizontal_indent.zero?
|
|
883
982
|
|
|
@@ -915,10 +1014,8 @@ module RSyntaxTree
|
|
|
915
1014
|
# TTB: parent's bottom → child's top
|
|
916
1015
|
if @polyline
|
|
917
1016
|
chi_x = child.horizontal_indent + child.content_width / 2
|
|
918
|
-
chi_y = child.empty_label? ? child.vertical_indent + child.content_height / 2 : child.vertical_indent + @global[:height_connector_to_text] / 2
|
|
919
|
-
|
|
920
1017
|
par_x = parent.horizontal_indent + parent.content_width / 2
|
|
921
|
-
par_y =
|
|
1018
|
+
chi_y, par_y = connector_edges(child, parent)
|
|
922
1019
|
|
|
923
1020
|
mid_x1 = chi_x
|
|
924
1021
|
mid_y1 = par_y + (chi_y - par_y) / 2
|
|
@@ -936,9 +1033,8 @@ module RSyntaxTree
|
|
|
936
1033
|
.sub(/PARY/, par_y.to_s)
|
|
937
1034
|
else
|
|
938
1035
|
x1 = child.horizontal_indent + child.content_width / 2
|
|
939
|
-
y1 = child.empty_label? ? child.vertical_indent + child.content_height / 2 : child.vertical_indent + @global[:height_connector_to_text] / 2
|
|
940
1036
|
x2 = parent.horizontal_indent + parent.content_width / 2
|
|
941
|
-
y2 =
|
|
1037
|
+
y1, y2 = connector_edges(child, parent)
|
|
942
1038
|
|
|
943
1039
|
line_data = @line_styles.sub(/X1/, x1.to_s)
|
|
944
1040
|
line_data = line_data.sub(/Y1/, y1.to_s)
|
|
@@ -24,6 +24,22 @@ module RSyntaxTree
|
|
|
24
24
|
def initialize(element_list, params)
|
|
25
25
|
@element_list = element_list
|
|
26
26
|
@params = params
|
|
27
|
+
|
|
28
|
+
# forest draws a tree: a parent joined to each daughter by an edge of
|
|
29
|
+
# its own, the root at the top. A derivation is neither — one rule
|
|
30
|
+
# across all the daughters at once, the root last — so the code this
|
|
31
|
+
# backend would emit is a correct tree of the same structure, which is
|
|
32
|
+
# a different figure rather than this one with something missing. What
|
|
33
|
+
# it drops elsewhere (colour, enclosures, decoration) leaves a figure
|
|
34
|
+
# that still says what the tree is. These two do not.
|
|
35
|
+
if params[:derivation] == true || params[:direction] == "btt"
|
|
36
|
+
what = params[:derivation] == true ? "a derivation" : "a tree drawn bottom to top"
|
|
37
|
+
raise RSTError.new(+"Error: TikZ output cannot draw #{what}",
|
|
38
|
+
code: :invalid_option,
|
|
39
|
+
hint: "forest joins each daughter with its own edge and puts the root " \
|
|
40
|
+
"at the top. Use png, svg or pdf for this figure.",
|
|
41
|
+
retryable: false)
|
|
42
|
+
end
|
|
27
43
|
end
|
|
28
44
|
|
|
29
45
|
# Generate TikZ forest code
|
data/lib/rsyntaxtree/utils.rb
CHANGED
|
@@ -61,30 +61,29 @@ FONT_FAMILIES = {
|
|
|
61
61
|
}.freeze
|
|
62
62
|
|
|
63
63
|
module FontFamily
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
# One order, used by the measuring and by the drawing alike. There was a
|
|
65
|
+
# second, with the CJK face moved to the front, which measurement switched to
|
|
66
|
+
# as soon as any character outside ASCII appeared anywhere in the figure while
|
|
67
|
+
# the drawing kept to this one — so Latin text was measured in a face it was
|
|
68
|
+
# not set in. Fontconfig reaches the CJK faces through this order anyway, the
|
|
69
|
+
# scripts that need them measuring the same either way, so the second order
|
|
70
|
+
# bought nothing and is gone.
|
|
71
|
+
def list(style)
|
|
72
|
+
FONT_FAMILIES.fetch(style.to_sym)
|
|
70
73
|
end
|
|
71
74
|
|
|
72
|
-
def for_svg(style
|
|
73
|
-
list(style
|
|
75
|
+
def for_svg(style)
|
|
76
|
+
list(style).map { |name| name.include?(" ") ? "'#{name}'" : name }.join(", ")
|
|
74
77
|
end
|
|
75
78
|
|
|
76
|
-
def for_pango(style
|
|
77
|
-
list(style
|
|
79
|
+
def for_pango(style)
|
|
80
|
+
list(style).join(", ")
|
|
78
81
|
end
|
|
79
82
|
|
|
80
83
|
module_function :list, :for_svg, :for_pango
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
class String
|
|
84
|
-
def contains_cjk?
|
|
85
|
-
!!(gsub(WHITESPACE_BLOCK, "") =~ /\p{Han}|\p{Katakana}|\p{Hiragana}|\p{Hangul}|[^\x01-\x7E]/)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
87
|
def contains_emoji?
|
|
89
88
|
!!(gsub(WHITESPACE_BLOCK, "").gsub(/\d/, "") =~ /\p{Emoji}/)
|
|
90
89
|
end
|
|
@@ -127,6 +126,19 @@ module FontMetrics
|
|
|
127
126
|
# 1.25x, so mixed-language documents had inconsistent spacing).
|
|
128
127
|
LINE_HEIGHT_FACTOR = 1.4
|
|
129
128
|
|
|
129
|
+
# How far below the baseline the visual centre of a line of text sits.
|
|
130
|
+
#
|
|
131
|
+
# Text is placed by its baseline, so anything meant to sit level with it — a
|
|
132
|
+
# box drawn around it, a rule beside it — has to know where the middle of the
|
|
133
|
+
# marks is. The answer is half the height of a capital, not half of whatever
|
|
134
|
+
# is written: centring on the marks themselves puts the middle of an `s`
|
|
135
|
+
# lower than the middle of a `G`, and centring on the whole cap-to-descender
|
|
136
|
+
# band sits low around digits and capitals, which reach nothing below the
|
|
137
|
+
# baseline and are most of what a label holds.
|
|
138
|
+
def visual_centre(font, fontsize)
|
|
139
|
+
get_metrics("X", font, fontsize, :normal, :normal).ink_above / 2.0
|
|
140
|
+
end
|
|
141
|
+
|
|
130
142
|
# Measures text with Pango, the same engine (and the same font fallback
|
|
131
143
|
# resolution) that librsvg uses to render the SVG output, so widths stay
|
|
132
144
|
# consistent with the drawn output for any script. `font` is a
|
|
@@ -152,5 +164,5 @@ module FontMetrics
|
|
|
152
164
|
@pango_layout ||= Pango::Layout.new(Pango::CairoFontMap.default.create_context)
|
|
153
165
|
end
|
|
154
166
|
|
|
155
|
-
module_function :get_metrics, :pango_layout
|
|
167
|
+
module_function :get_metrics, :pango_layout, :visual_centre
|
|
156
168
|
end
|
data/lib/rsyntaxtree/version.rb
CHANGED
data/lib/rsyntaxtree.rb
CHANGED
|
@@ -58,8 +58,9 @@ OPTION_VALUES = {
|
|
|
58
58
|
fontstyle: ["sans", "serif", "cjk", "mono", "noto-sans", "noto-serif", "noto-sans-mono", "cjk zenhei"],
|
|
59
59
|
color: %w[modern traditional gray grey off none on true false],
|
|
60
60
|
tidy: %w[off symmetric low medium high on compact true false],
|
|
61
|
-
direction: %w[ttb ltr],
|
|
62
|
-
hyphen: %w[markup literal]
|
|
61
|
+
direction: %w[ttb ltr btt],
|
|
62
|
+
hyphen: %w[markup literal],
|
|
63
|
+
derivation: %w[on off true false yes no 0 1 none]
|
|
63
64
|
}.freeze
|
|
64
65
|
|
|
65
66
|
NUMERIC_RANGES = {
|
|
@@ -86,7 +87,8 @@ DEFAULT_OPTS = {
|
|
|
86
87
|
tidy: "off",
|
|
87
88
|
hspacing: 1.0,
|
|
88
89
|
direction: "ttb",
|
|
89
|
-
hyphen: "markup"
|
|
90
|
+
hyphen: "markup",
|
|
91
|
+
derivation: "off"
|
|
90
92
|
}.freeze
|
|
91
93
|
|
|
92
94
|
# A parse or generation failure. `message` is the human-readable text the
|
|
@@ -202,7 +204,7 @@ module RSyntaxTree
|
|
|
202
204
|
else
|
|
203
205
|
"off"
|
|
204
206
|
end
|
|
205
|
-
when :tidy_nest, :symmetrize, :transparent, :polyline, :hide_default_connectors, :mirror
|
|
207
|
+
when :tidy_nest, :symmetrize, :transparent, :polyline, :hide_default_connectors, :mirror, :derivation
|
|
206
208
|
new_params[key] = switched_on?(value)
|
|
207
209
|
when :color
|
|
208
210
|
new_params[key] = case value.to_s
|
|
@@ -234,7 +236,6 @@ module RSyntaxTree
|
|
|
234
236
|
else :sans
|
|
235
237
|
end
|
|
236
238
|
fontset[:family] = FontFamily.for_pango(style)
|
|
237
|
-
fontset[:family_cjk] = FontFamily.for_pango(style, cjk_priority: style != :cjk)
|
|
238
239
|
new_params[:fontstyle] = style.to_s
|
|
239
240
|
else
|
|
240
241
|
new_params[key] = value
|
|
@@ -256,12 +257,43 @@ module RSyntaxTree
|
|
|
256
257
|
# fall back to the merged default style otherwise
|
|
257
258
|
if fontset[:family].nil?
|
|
258
259
|
fontset[:family] = FontFamily.for_pango(@params[:fontstyle])
|
|
259
|
-
fontset[:family_cjk] = FontFamily.for_pango(@params[:fontstyle], cjk_priority: true)
|
|
260
260
|
end
|
|
261
261
|
@params[:fontset] = fontset
|
|
262
262
|
single_x_metrics = FontMetrics.get_metrics("X", fontset[:family], @params[:fontsize], :normal, :normal)
|
|
263
263
|
@global = {}
|
|
264
|
+
# A derivation is written down the page: the rule that joins the
|
|
265
|
+
# premises is a horizontal line across them, and the result sits under
|
|
266
|
+
# it. Laid out left to right there is nothing for that line to span, and
|
|
267
|
+
# the drawing came out with rules struck through the categories. There
|
|
268
|
+
# is no left-to-right convention for a derivation to fall back on, so
|
|
269
|
+
# the combination is refused rather than approximated.
|
|
270
|
+
if @params[:derivation] == true && @params[:direction] == "ltr"
|
|
271
|
+
raise RSTError.new(+"Error: a derivation cannot be drawn left to right",
|
|
272
|
+
code: :invalid_option,
|
|
273
|
+
hint: "A derivation runs down the page. Use direction ttb or btt, " \
|
|
274
|
+
"or turn derivation off.",
|
|
275
|
+
retryable: false)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Hiding the default connectors draws them in the background colour
|
|
279
|
+
# rather than skipping them. A derivation's rules are drawn as connectors
|
|
280
|
+
# but they are the figure itself, not a default the drawing adds, so
|
|
281
|
+
# hiding them leaves rows of categories floating with nothing joining
|
|
282
|
+
# them. Refused for the same reason as left to right.
|
|
283
|
+
if @params[:derivation] == true && @params[:hide_default_connectors] == true
|
|
284
|
+
raise RSTError.new(+"Error: a derivation's rules cannot be hidden",
|
|
285
|
+
code: :invalid_option,
|
|
286
|
+
hint: "The rules are what a derivation is drawn with, not a " \
|
|
287
|
+
"connector added to it. Turn off hide default connectors, " \
|
|
288
|
+
"or turn derivation off.",
|
|
289
|
+
retryable: false)
|
|
290
|
+
end
|
|
291
|
+
|
|
264
292
|
@global[:single_x_metrics] = single_x_metrics
|
|
293
|
+
# A derivation labels each step with the rule it applied, written at the
|
|
294
|
+
# right end of the line. Elements need to know, because the name is taken
|
|
295
|
+
# out of the label before it is measured.
|
|
296
|
+
@global[:derivation] = @params[:derivation] == true
|
|
265
297
|
@global[:height_connector_to_text] = single_x_metrics.height / 2.0
|
|
266
298
|
@global[:single_line_height] = single_x_metrics.height * 2.0
|
|
267
299
|
@global[:width_half_x] = single_x_metrics.width / 2.0
|
data/rsyntaxtree.gemspec
CHANGED
|
@@ -12,10 +12,18 @@ Gem::Specification.new do |s|
|
|
|
12
12
|
s.description = "Syntax tree generator made with Ruby"
|
|
13
13
|
s.licenses = ["MIT"]
|
|
14
14
|
s.required_ruby_version = ">= 3.2.0"
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# What the gem carries is what someone installing it needs: the library, the
|
|
16
|
+
# command, the notation reference the command prints, and the files that say
|
|
17
|
+
# what this is, how to cite it and under what licence. The rest of the
|
|
18
|
+
# repository is for working on it — the documentation site and its gallery,
|
|
19
|
+
# the scripts that build them, the tests, the CI and container setup, the
|
|
20
|
+
# bundle and the rake tasks — and whoever wants those clones the repository.
|
|
21
|
+
development = ["docs/", "dev/", "img/", "test/", ".github/"]
|
|
22
|
+
development_files = [".gitattributes", ".gitignore", ".ruby-version",
|
|
23
|
+
"Dockerfile", "Gemfile", "Rakefile"]
|
|
17
24
|
s.files = `git ls-files`.split("\n").reject do |path|
|
|
18
|
-
path.start_with?(
|
|
25
|
+
development.any? { |dir| path.start_with?(dir) } ||
|
|
26
|
+
development_files.include?(path)
|
|
19
27
|
end
|
|
20
28
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
21
29
|
s.require_paths = ["lib"]
|
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.
|
|
4
|
+
version: 1.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yoichiro Hasebe
|
|
@@ -143,18 +143,10 @@ executables:
|
|
|
143
143
|
extensions: []
|
|
144
144
|
extra_rdoc_files: []
|
|
145
145
|
files:
|
|
146
|
-
- ".gitattributes"
|
|
147
|
-
- ".github/FUNDING.yml"
|
|
148
|
-
- ".github/workflows/test.yml"
|
|
149
|
-
- ".gitignore"
|
|
150
|
-
- ".ruby-version"
|
|
151
146
|
- CHANGELOG.md
|
|
152
147
|
- CITATION.cff
|
|
153
|
-
- Dockerfile
|
|
154
|
-
- Gemfile
|
|
155
148
|
- LICENSE
|
|
156
149
|
- README.md
|
|
157
|
-
- Rakefile
|
|
158
150
|
- bin/rsyntaxtree
|
|
159
151
|
- lib/rsyntaxtree.rb
|
|
160
152
|
- lib/rsyntaxtree/base_graph.rb
|
data/.gitattributes
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# Text files are stored with LF and checked out with LF, whatever the
|
|
2
|
-
# editor on the machine does. Two of the documentation pages had drifted
|
|
3
|
-
# to CRLF, which turned a three-line edit into a whole-file diff.
|
|
4
|
-
* text=auto eol=lf
|
|
5
|
-
|
|
6
|
-
*.png binary
|
|
7
|
-
*.jpg binary
|
|
8
|
-
*.gif binary
|
|
9
|
-
*.pdf binary
|
|
10
|
-
*.gem binary
|
data/.github/FUNDING.yml
DELETED
data/.github/workflows/test.yml
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
name: Test
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [master]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [master]
|
|
8
|
-
workflow_dispatch:
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
test:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
strategy:
|
|
14
|
-
matrix:
|
|
15
|
-
ruby-version: ["3.2", "3.4"]
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v4
|
|
18
|
-
|
|
19
|
-
- name: Install system dependencies
|
|
20
|
-
run: |
|
|
21
|
-
sudo apt-get update
|
|
22
|
-
sudo apt-get install -y \
|
|
23
|
-
libpango1.0-dev librsvg2-dev libmagickwand-dev \
|
|
24
|
-
libgirepository1.0-dev gobject-introspection \
|
|
25
|
-
fonts-noto-core fonts-noto-cjk
|
|
26
|
-
sudo fc-cache -f
|
|
27
|
-
|
|
28
|
-
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
29
|
-
uses: ruby/setup-ruby@v1
|
|
30
|
-
with:
|
|
31
|
-
ruby-version: ${{ matrix.ruby-version }}
|
|
32
|
-
bundler-cache: true
|
|
33
|
-
|
|
34
|
-
- name: Run tests
|
|
35
|
-
run: bundle exec rake test
|
data/.gitignore
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
pkg/
|
|
2
|
-
tmp/
|
|
3
|
-
doc/
|
|
4
|
-
.DS_Store
|
|
5
|
-
Gemfile.lock
|
|
6
|
-
|
|
7
|
-
*.bak
|
|
8
|
-
*.~
|
|
9
|
-
*.log
|
|
10
|
-
*.gem
|
|
11
|
-
log/
|
|
12
|
-
logs/
|
|
13
|
-
|
|
14
|
-
bin/rsync_code
|
|
15
|
-
bin/linktree.rb
|
|
16
|
-
tags
|
|
17
|
-
.tags
|
|
18
|
-
.rubocop.yml
|
|
19
|
-
.rubocop_todo.yml
|
|
20
|
-
.solargraph.yml
|
|
21
|
-
|
|
22
|
-
CLAUDE.md
|
|
23
|
-
|
|
24
|
-
# git filter-branch leaves this behind; it is machinery, not source.
|
|
25
|
-
.git-rewrite/
|
data/.ruby-version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
4.0.1
|
data/Dockerfile
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
FROM ruby:3.3-alpine
|
|
2
|
-
ENV WORKSPACE=/rsyntaxtree
|
|
3
|
-
WORKDIR $WORKSPACE
|
|
4
|
-
|
|
5
|
-
RUN apk update && \
|
|
6
|
-
apk upgrade && \
|
|
7
|
-
apk add --no-cache linux-headers libxml2-dev make gcc libc-dev bash && \
|
|
8
|
-
apk add --no-cache librsvg librsvg-dev pango-dev imagemagick imagemagick-dev xz-dev libbz2 && \
|
|
9
|
-
apk add --no-cache gobject-introspection gobject-introspection-dev && \
|
|
10
|
-
apk add --no-cache -t .build-packages --no-cache build-base curl-dev wget gcompat && \
|
|
11
|
-
apk add --no-cache font-noto font-noto-cjk font-noto-cjk-extra \
|
|
12
|
-
font-noto-arabic font-noto-naskh-arabic font-noto-hebrew \
|
|
13
|
-
font-noto-devanagari font-noto-thai font-noto-khmer \
|
|
14
|
-
font-dejavu
|
|
15
|
-
|
|
16
|
-
# Noto Sans Math (pulled in by font-noto) claims the Arabic block but carries no
|
|
17
|
-
# joining rules, and fontconfig ranks it above Noto Sans Arabic — Arabic then
|
|
18
|
-
# renders as isolated letterforms. Keep the font, since trees use its
|
|
19
|
-
# mathematical alphanumerics (the little v of vP), but drop Arabic from it.
|
|
20
|
-
COPY dev/fontconfig/99-noto-math-no-arabic.conf /etc/fonts/conf.d/99-noto-math-no-arabic.conf
|
|
21
|
-
|
|
22
|
-
# Emoji: the monochrome Noto Emoji, not Alpine's font-noto-emoji (which is the
|
|
23
|
-
# colour NotoColorEmoji). Cairo does not rasterise its bitmap glyphs in this
|
|
24
|
-
# pipeline, so emoji would silently fall back to whatever outline font happens
|
|
25
|
-
# to cover them. Pinned to a google/fonts commit so the gallery stays stable.
|
|
26
|
-
ARG NOTO_EMOJI_SHA=b979dba422e445492b0eb9951ac52ee0b4d648c3
|
|
27
|
-
ARG NOTO_EMOJI_SHA256=de6c18832938afc99caf132b39d6a30a19bac7f2e812e28db2535b4608d27551
|
|
28
|
-
RUN mkdir -p /usr/share/fonts/noto-emoji && \
|
|
29
|
-
wget -q -O /usr/share/fonts/noto-emoji/NotoEmoji-Regular.ttf \
|
|
30
|
-
"https://raw.githubusercontent.com/google/fonts/${NOTO_EMOJI_SHA}/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf" && \
|
|
31
|
-
echo "${NOTO_EMOJI_SHA256} /usr/share/fonts/noto-emoji/NotoEmoji-Regular.ttf" | sha256sum -c -
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
ADD Gemfile $WORKSPACE
|
|
35
|
-
ADD rsyntaxtree.gemspec $WORKSPACE
|
|
36
|
-
RUN bundle install -j4
|
|
37
|
-
|
|
38
|
-
RUN fc-cache -fv
|
|
39
|
-
|
|
40
|
-
ADD . $WORKSPACE
|
|
41
|
-
CMD ["/bin/bash"]
|
data/Gemfile
DELETED
data/Rakefile
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/gem_tasks"
|
|
4
|
-
require "rake/testtask"
|
|
5
|
-
require "yaml"
|
|
6
|
-
require_relative 'lib/rsyntaxtree'
|
|
7
|
-
require_relative 'lib/rsyntaxtree/utils'
|
|
8
|
-
|
|
9
|
-
# task default: "test"
|
|
10
|
-
|
|
11
|
-
Rake::TestTask.new do |task|
|
|
12
|
-
task.pattern = "test/*_test.rb"
|
|
13
|
-
task.warning = false
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Gem packaging preserves on-disk file modes. Owner-only permissions (which a
|
|
17
|
-
# Dropbox-synced checkout tends to produce) ship files that are unreadable, and
|
|
18
|
-
# a CLI that is unexecutable, for anyone but the owner after `sudo gem install`.
|
|
19
|
-
# Normalize every tracked file before the gem is built: a file is executable
|
|
20
|
-
# only if it is a command (bin/, exe/) or a script with a shebang; everything
|
|
21
|
-
# else is data and gets 0644. Deciding by path/content rather than by the
|
|
22
|
-
# current mode also drops stray executable bits picked up from the filesystem.
|
|
23
|
-
task :normalize_permissions do
|
|
24
|
-
`git ls-files -z`.split("\x0").each do |f|
|
|
25
|
-
next unless File.file?(f)
|
|
26
|
-
|
|
27
|
-
executable = f.start_with?("bin/", "exe/") ||
|
|
28
|
-
File.open(f, "rb") { |io| io.read(2) } == "#!"
|
|
29
|
-
File.chmod(executable ? 0o755 : 0o644, f)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
Rake::Task["build"].enhance([:normalize_permissions])
|
|
34
|
-
|
|
35
|
-
# For trying something out. It does NOT reproduce the committed gallery:
|
|
36
|
-
# fontconfig resolves a family to whatever this machine has, so the same input
|
|
37
|
-
# measures differently here than in the image the figures were drawn with. Run
|
|
38
|
-
# over docs/assets it rewrites every PNG and about half the SVGs, none of it an
|
|
39
|
-
# intended change. Use docker_generate to update the gallery.
|
|
40
|
-
desc "Generate SVG and PNG example images locally (not for updating the gallery)"
|
|
41
|
-
task :generate do
|
|
42
|
-
require_relative "dev/generate_examples"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# What a language model is given: the reference, the manual and every example,
|
|
46
|
-
# built from the files those already live in. Run it after changing any of
|
|
47
|
-
# them; llm_payload_test.rb fails if the built files are out of date.
|
|
48
|
-
desc "Build the notation payload for language models (gem and site)"
|
|
49
|
-
task :llm_payload do
|
|
50
|
-
ruby "dev/generate_llm_payload.rb"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
desc "Record the pixel size of each gallery figure for the examples page"
|
|
54
|
-
task :figure_sizes do
|
|
55
|
-
ruby "dev/stamp_figure_sizes.rb"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
desc "Docker image Build"
|
|
59
|
-
task :docker_build do
|
|
60
|
-
`docker build ./ -t rsyntaxtree_devel`
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# The gallery is drawn here and nowhere else, so that a figure depends on the
|
|
64
|
-
# input and the options rather than on the fonts of whoever regenerated it.
|
|
65
|
-
desc "Generate SVG and PNG example images using Docker image (the gallery)"
|
|
66
|
-
task :docker_generate do
|
|
67
|
-
docpath = File.expand_path(File.join(__dir__, "docs"))
|
|
68
|
-
`docker build ./ -t rsyntaxtree_devel`
|
|
69
|
-
`docker run --rm -v #{docpath}:/rsyntaxtree/hostdocs rsyntaxtree_devel ruby /rsyntaxtree/dev/generate_examples.rb /rsyntaxtree/hostdocs`
|
|
70
|
-
`cat #{docpath}/generate_examples.log`
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# The examples page scales every figure by the size recorded here, so the
|
|
74
|
-
# record has to be refreshed whenever the figures are.
|
|
75
|
-
["generate", "docker_generate"].each do |name|
|
|
76
|
-
Rake::Task[name].enhance { Rake::Task["figure_sizes"].invoke }
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Add new task for macOS environment configuration
|
|
80
|
-
|
|
81
|
-
desc "Configure Bundler build options for macOS"
|
|
82
|
-
task :setup_macos do
|
|
83
|
-
require "rbconfig"
|
|
84
|
-
host_os = RbConfig::CONFIG['host_os']
|
|
85
|
-
|
|
86
|
-
# Check if the host OS is macOS (Darwin)
|
|
87
|
-
|
|
88
|
-
if host_os =~ /darwin/
|
|
89
|
-
puts "macOS detected. Setting up build options for native extensions..."
|
|
90
|
-
|
|
91
|
-
gems_with_options = {
|
|
92
|
-
"gobject-introspection" => '--with-ldflags=-Wl,-undefined,dynamic_lookup',
|
|
93
|
-
"cairo-gobject" => '--with-ldflags=-Wl,-undefined,dynamic_lookup',
|
|
94
|
-
"gio2" => '--with-ldflags=-Wl,-undefined,dynamic_lookup'
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
# Configure each gem with the necessary ldflags option using Bundler config command
|
|
98
|
-
|
|
99
|
-
gems_with_options.each do |gem_name, flags|
|
|
100
|
-
command = "bundle config build.#{gem_name} \"#{flags}\""
|
|
101
|
-
puts "Executing: #{command}"
|
|
102
|
-
unless system(command)
|
|
103
|
-
abort("Failed to execute command: #{command}")
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
puts "macOS setup complete. Please run 'bundle install' after this."
|
|
108
|
-
else
|
|
109
|
-
puts "This setup task is intended for macOS environments. Current OS: #{host_os}"
|
|
110
|
-
end
|
|
111
|
-
end
|