rsyntaxtree 1.12.0 → 1.13.1
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 +38 -0
- data/bin/rsyntaxtree +5 -3
- data/lib/rsyntaxtree/base_graph.rb +177 -4
- data/lib/rsyntaxtree/element.rb +95 -15
- data/lib/rsyntaxtree/lsif_graph.rb +16 -3
- data/lib/rsyntaxtree/notation_examples.md +45 -58
- 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 +50 -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: '0804e9119e0ebb089b4109ae05c9353180362bd6e86fddbf2e4da00db3b08e18'
|
|
4
|
+
data.tar.gz: '01867ffda61d7f81fd3d81a9f6ad44ba83eda4f7d89d1b3b05d53b196bc23fa1'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 990e4522fe0e0694d009eddebc2b3b2af98e5beda1f3e2daf9fbf1250199cef31b981215d1ae7db3b711d0030826e2445c7011f427223f8e081bffb8d76451ae
|
|
7
|
+
data.tar.gz: fe620d8c1fe64c289b18d853d6663ceaae5ea8a7486a5f0cf0ce578f8c0e9073e48251f0d7a933d0c7afa9ee7fce896dd6cbf00f3499c8d5314278e9cc0e0b57
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.13.1] - 2026-08
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `^` at the head of a leaf draws the triangle it asks for. The mark may be
|
|
7
|
+
written on the node's label or on the leaf's own text — `[^NP cats]` and
|
|
8
|
+
`[NP ^cats]` — and the documentation, the notation reference and the gallery
|
|
9
|
+
all use both, but only the first was drawn: the second lost its caret to the
|
|
10
|
+
parser and then a bar was drawn under it. The two now produce the same figure
|
|
11
|
+
in every connector style, and LSIF records the edge as a triangle. Three
|
|
12
|
+
gallery figures change accordingly, each toward what it was written to show.
|
|
13
|
+
- An option given as an empty string is read as an option not given, rather
|
|
14
|
+
than as a value no list contains. An HTML form posts a field for every
|
|
15
|
+
control it carries, and a control with nothing selected posts the empty
|
|
16
|
+
string, so a form that has outlived one of its own controls sends that field
|
|
17
|
+
empty alongside the rest. Since option validation arrived in 1.11.0 that
|
|
18
|
+
failed the whole request: the web UI's Download buttons answered 500 for
|
|
19
|
+
every input, because the form still read a `format` select the page no longer
|
|
20
|
+
had. A value that is actually wrong is still rejected.
|
|
21
|
+
|
|
22
|
+
## [1.13.0] - 2026-08
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Derivations. A derivation puts the words first and the result last, and
|
|
26
|
+
joins what each step combines with one rule drawn across all of it rather
|
|
27
|
+
than with a line to each daughter. `derivation` draws the rules and
|
|
28
|
+
`direction: btt` turns the tree over; together they give the format
|
|
29
|
+
categorial grammar is written in, and `derivation` on its own marks the
|
|
30
|
+
spans of an ordinary tree. The name of each step rides in the label after a
|
|
31
|
+
column break and is set beside the end of its rule. A derivation runs down
|
|
32
|
+
the page and is drawn with its rules, so `direction: ltr` and hiding the
|
|
33
|
+
default connectors are refused rather than approximated.
|
|
34
|
+
- `direction: btt`, the layout turned over, leaves first.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Connectors follow the tree when it is turned over. They kept the ends they
|
|
38
|
+
have when the root is at the top, so every line ran from above the daughter,
|
|
39
|
+
through its label and the mother's, to below the mother.
|
|
40
|
+
|
|
3
41
|
## [1.12.0] - 2026-08
|
|
4
42
|
|
|
5
43
|
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
|
|
@@ -209,7 +221,12 @@ module RSyntaxTree
|
|
|
209
221
|
else
|
|
210
222
|
parent = @element_list.get_id(target.parent)
|
|
211
223
|
|
|
212
|
-
|
|
224
|
+
# A leaf with nothing drawn over it is pulled up against its node; one
|
|
225
|
+
# with a triangle over it needs the room the triangle takes. Which is
|
|
226
|
+
# which is the same question the connector asks, so it is asked the same
|
|
227
|
+
# way — reading only the leaf's own mark left `[^NP cats]` a triangle
|
|
228
|
+
# squashed into a gap that had been closed.
|
|
229
|
+
vertical_indent = if !triangle_asked_for?(parent, target) &&
|
|
213
230
|
(@leafstyle == "nothing" || @leafstyle == "none") &&
|
|
214
231
|
ETYPE_LEAF == target.type && parent.children.size == 1
|
|
215
232
|
if @direction == "ltr"
|
|
@@ -234,9 +251,91 @@ module RSyntaxTree
|
|
|
234
251
|
end
|
|
235
252
|
target.height = accum_array.max - target.vertical_indent
|
|
236
253
|
accum_array.max
|
|
254
|
+
end.tap { level_derivation_rows if id == 1 && @derivation }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# In a derivation every premise is given at the start, so the words stand in
|
|
258
|
+
# one row and each step sits below everything it draws on. A node's row is
|
|
259
|
+
# therefore its distance from the words rather than its distance from the
|
|
260
|
+
# root: a step joining a category to a derivation three deep goes below
|
|
261
|
+
# both. Placed by depth from the root instead, the words come out in a
|
|
262
|
+
# staircase, each at the depth of its own branch.
|
|
263
|
+
#
|
|
264
|
+
# Run after the ordinary placement, so the row heights and the drop between
|
|
265
|
+
# them are the ones that pass settled on.
|
|
266
|
+
# Levelled twice, and the two are not the same pass. The first runs inside
|
|
267
|
+
# the layout, because tidy packs subtrees by their contours and a contour is
|
|
268
|
+
# read off the vertical positions: rows it has not seen levelled pack as
|
|
269
|
+
# though the figure were a tree, and two rules of one row come out
|
|
270
|
+
# overlapping. The second runs after the layout has been turned over, since
|
|
271
|
+
# turning it over sets each box against its own bottom edge, which leaves a
|
|
272
|
+
# tall label — a feature matrix, say — standing proud of its row. Counting
|
|
273
|
+
# rows from the end the words are at is what makes the second pass the same
|
|
274
|
+
# reckoning as the first, read the other way up.
|
|
275
|
+
def level_derivation_rows(flipped: false)
|
|
276
|
+
root_height = subtree_height(1)
|
|
277
|
+
rows = {}
|
|
278
|
+
tallest = Hash.new(0.0)
|
|
279
|
+
@element_list.get_elements.each do |e|
|
|
280
|
+
height = subtree_height(e.id)
|
|
281
|
+
row = flipped ? height : root_height - height
|
|
282
|
+
rows[e.id] = row
|
|
283
|
+
tallest[row] = [tallest[row], e.content_height].max
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
drop = @global[:height_connector].to_f
|
|
287
|
+
top = {}
|
|
288
|
+
y = 0.0
|
|
289
|
+
tallest.keys.sort.each do |row|
|
|
290
|
+
top[row] = y
|
|
291
|
+
y += tallest[row] + drop
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
@element_list.get_elements.each { |e| e.vertical_indent = top[rows[e.id]] }
|
|
295
|
+
|
|
296
|
+
# Which row each element ended up in, kept for the drawing: a rule belongs
|
|
297
|
+
# between two rows, not between two labels. Placed from the label instead,
|
|
298
|
+
# a step whose result is taller than its neighbours — a feature matrix
|
|
299
|
+
# among plain categories — had its rule drawn at a height of its own while
|
|
300
|
+
# the rules beside it stayed on the row.
|
|
301
|
+
@derivation_rows = rows
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# How far a row reaches, read from the labels as they stand. Taken when the
|
|
305
|
+
# rows were levelled it would be the measured height, and the drawing works
|
|
306
|
+
# to a slightly different one.
|
|
307
|
+
def derivation_row_extent(row)
|
|
308
|
+
@derivation_extents ||= {}
|
|
309
|
+
@derivation_extents[row] ||= begin
|
|
310
|
+
members = @element_list.get_elements.select { |e| @derivation_rows[e.id] == row }
|
|
311
|
+
[members.map(&:vertical_indent).min,
|
|
312
|
+
members.map { |e| e.vertical_indent + e.content_height }.max]
|
|
237
313
|
end
|
|
238
314
|
end
|
|
239
315
|
|
|
316
|
+
# The rule for a step runs between the row its result sits in and the row
|
|
317
|
+
# holding the last of its premises — always the next row along, since a step
|
|
318
|
+
# is one taller than the tallest thing it draws on.
|
|
319
|
+
def derivation_rule_band(parent)
|
|
320
|
+
return nil unless @derivation_rows
|
|
321
|
+
|
|
322
|
+
row = @derivation_rows[parent.id]
|
|
323
|
+
neighbour = row + (@direction == "btt" ? -1 : 1)
|
|
324
|
+
return nil unless @derivation_rows.value?(neighbour)
|
|
325
|
+
|
|
326
|
+
here = derivation_row_extent(row)
|
|
327
|
+
there = derivation_row_extent(neighbour)
|
|
328
|
+
@direction == "btt" ? [there[1], here[0]] : [here[1], there[0]]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Steps from +id+ down to the furthest word under it. A word is zero.
|
|
332
|
+
def subtree_height(id)
|
|
333
|
+
node = @element_list.get_id(id)
|
|
334
|
+
return 0 if node.children.empty?
|
|
335
|
+
|
|
336
|
+
1 + node.children.map { |c| subtree_height(c) }.max
|
|
337
|
+
end
|
|
338
|
+
|
|
240
339
|
def make_balance(id = 1)
|
|
241
340
|
target = @element_list.get_id(id)
|
|
242
341
|
if target.children.empty?
|
|
@@ -301,27 +400,53 @@ module RSyntaxTree
|
|
|
301
400
|
end
|
|
302
401
|
end
|
|
303
402
|
|
|
403
|
+
# Whether a `^` has asked for a triangle over this leaf. It may be written
|
|
404
|
+
# at the head of the node's label or at the head of the leaf's own text —
|
|
405
|
+
# `[^NP cats]` and `[NP ^cats]` — and both forms are in the gallery. Only
|
|
406
|
+
# the first was honoured here, and the second was dropped without a word:
|
|
407
|
+
# the caret vanished from the text and a bar was drawn, so an example named
|
|
408
|
+
# for its triangles had none. The rest of the layout already reads the
|
|
409
|
+
# leaf's own flag — a leaf marked this way keeps the gap a triangle needs
|
|
410
|
+
# even when connectors are off — which is what left the two sides of one
|
|
411
|
+
# rule disagreeing.
|
|
412
|
+
#
|
|
413
|
+
# Asked of a leaf only. A `^` on an internal node is that node's own mark,
|
|
414
|
+
# and it is honoured when that node is in turn the parent here.
|
|
415
|
+
def triangle_asked_for?(parent, child)
|
|
416
|
+
parent.triangle || (ETYPE_LEAF == child.type && child.triangle)
|
|
417
|
+
end
|
|
418
|
+
|
|
304
419
|
def draw_connector(id = 1)
|
|
305
420
|
parent = @element_list.get_id(id)
|
|
306
421
|
children = parent.children.map { |c| @element_list.get_id(c) }
|
|
307
422
|
|
|
423
|
+
# A derivation joins its premises with one rule drawn across all of them,
|
|
424
|
+
# not with a line to each. The rule replaces the lines, so a tree is drawn
|
|
425
|
+
# this way throughout or not at all.
|
|
426
|
+
if @derivation && !children.empty?
|
|
427
|
+
rule_to_parent(parent, children)
|
|
428
|
+
parent.children.each { |c| draw_connector(c) }
|
|
429
|
+
return
|
|
430
|
+
end
|
|
431
|
+
|
|
308
432
|
if children.size == 1
|
|
309
433
|
child = children[0]
|
|
434
|
+
forced = triangle_asked_for?(parent, child)
|
|
310
435
|
case @leafstyle
|
|
311
436
|
when "auto"
|
|
312
|
-
if
|
|
437
|
+
if forced || child.contains_phrase
|
|
313
438
|
triangle_to_parent(parent, child)
|
|
314
439
|
else
|
|
315
440
|
line_to_parent(parent, child)
|
|
316
441
|
end
|
|
317
442
|
when "bar"
|
|
318
|
-
if
|
|
443
|
+
if forced
|
|
319
444
|
triangle_to_parent(parent, child)
|
|
320
445
|
else
|
|
321
446
|
line_to_parent(parent, child)
|
|
322
447
|
end
|
|
323
448
|
when "nothing", "none"
|
|
324
|
-
if
|
|
449
|
+
if forced
|
|
325
450
|
triangle_to_parent(parent, child)
|
|
326
451
|
elsif ETYPE_LEAF != child.type
|
|
327
452
|
line_to_parent(parent, child)
|
|
@@ -345,6 +470,20 @@ module RSyntaxTree
|
|
|
345
470
|
(children_indent << target_indent).min
|
|
346
471
|
end
|
|
347
472
|
|
|
473
|
+
# Width to keep clear to the right of a step's rule for the name beside it,
|
|
474
|
+
# with a gap between the name and whatever follows. Zero unless the tree is
|
|
475
|
+
# drawn as a derivation and the step has a name.
|
|
476
|
+
def rule_name_room(element)
|
|
477
|
+
return 0 unless @derivation
|
|
478
|
+
|
|
479
|
+
name = element.rule_name
|
|
480
|
+
return 0 if name.nil? || name.empty?
|
|
481
|
+
|
|
482
|
+
size = (@fontsize * 0.8).round(2)
|
|
483
|
+
width = FontMetrics.get_metrics(name, @fontset[:family], size, :normal, :normal).width
|
|
484
|
+
width + @global[:width_half_x]
|
|
485
|
+
end
|
|
486
|
+
|
|
348
487
|
def get_rightmost(id = 1)
|
|
349
488
|
target = @element_list.get_id(id)
|
|
350
489
|
target_right_end = target.horizontal_indent + target.content_width
|
|
@@ -363,6 +502,19 @@ module RSyntaxTree
|
|
|
363
502
|
end
|
|
364
503
|
end
|
|
365
504
|
|
|
505
|
+
# Flip the laid-out tree vertically: the root ends up at the bottom and the
|
|
506
|
+
# leaves at the top, which is how a derivation is written — the words at the
|
|
507
|
+
# top, each step below the material it combines. Done after the layout is
|
|
508
|
+
# final, like the horizontal flip, so connectors, triangles, paths and
|
|
509
|
+
# region shades follow from the coordinates without knowing about it.
|
|
510
|
+
def flip_vertical
|
|
511
|
+
elements = @element_list.get_elements
|
|
512
|
+
max_bottom = elements.map { |e| e.vertical_indent + e.content_height }.max
|
|
513
|
+
elements.each do |e|
|
|
514
|
+
e.vertical_indent = max_bottom - (e.vertical_indent + e.content_height)
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
366
518
|
# Flip the laid-out tree horizontally (RTL linguistics convention: the
|
|
367
519
|
# first word sits at the right edge). Connectors, triangles, polylines,
|
|
368
520
|
# movement paths, and region shades all derive from element coordinates,
|
|
@@ -425,9 +577,23 @@ module RSyntaxTree
|
|
|
425
577
|
x0 -= ext
|
|
426
578
|
x1 += ext
|
|
427
579
|
end
|
|
580
|
+
# A step's name is drawn past the right end of its rule, and the rule
|
|
581
|
+
# reaches the full width of what the step combines. Packing sees only the
|
|
582
|
+
# labels, so without this the neighbour is pulled up against the name and
|
|
583
|
+
# two steps read as one long rule.
|
|
584
|
+
x1 = [x1, subtree_right(node.id) + rule_name_room(node)].max if @derivation
|
|
428
585
|
[node.vertical_indent, node.vertical_indent + node.content_height, x0, x1]
|
|
429
586
|
end
|
|
430
587
|
|
|
588
|
+
# Right edge of the subtree rooted at +id+, from wherever the layout has
|
|
589
|
+
# put things so far.
|
|
590
|
+
def subtree_right(id)
|
|
591
|
+
node = @element_list.get_id(id)
|
|
592
|
+
right = node.horizontal_indent + node.content_width
|
|
593
|
+
node.children.each { |c| right = [right, subtree_right(c)].max }
|
|
594
|
+
right
|
|
595
|
+
end
|
|
596
|
+
|
|
431
597
|
# Effective-rect list of the subtree rooted at +id+, as [y0, y1, x0, x1].
|
|
432
598
|
# A region-shaded (%) node additionally contributes the shade's padded
|
|
433
599
|
# bounding rect so that neighboring subtrees keep clear of the plane.
|
|
@@ -900,6 +1066,10 @@ module RSyntaxTree
|
|
|
900
1066
|
finalize_ltr if @direction == "ltr"
|
|
901
1067
|
|
|
902
1068
|
# RTL flip (mirror option): after the layout is final, before drawing
|
|
1069
|
+
# btt is the top-to-bottom layout turned over, so the layout runs as
|
|
1070
|
+
# usual and the flip comes after it.
|
|
1071
|
+
flip_vertical if @direction == "btt"
|
|
1072
|
+
level_derivation_rows(flipped: true) if @derivation && @direction == "btt"
|
|
903
1073
|
mirror_layout if @mirror
|
|
904
1074
|
|
|
905
1075
|
draw_elements
|
|
@@ -915,6 +1085,9 @@ module RSyntaxTree
|
|
|
915
1085
|
max_x = r if r > max_x
|
|
916
1086
|
max_y = b if b > max_y
|
|
917
1087
|
end
|
|
1088
|
+
# A rule's name is set beside the rule, past the right edge of every box
|
|
1089
|
+
# the loop above measured.
|
|
1090
|
+
max_x = @rule_name_edge if @rule_name_edge.to_f > max_x
|
|
918
1091
|
width = max_x + @global[:h_gap_between_nodes]
|
|
919
1092
|
height = max_y
|
|
920
1093
|
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,23 +69,35 @@ 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]
|
|
85
|
+
forced = triangle_asked_for?(parent, child)
|
|
74
86
|
case @leafstyle
|
|
75
87
|
when "auto"
|
|
76
|
-
if
|
|
88
|
+
if forced || child.contains_phrase
|
|
77
89
|
triangle_to_parent(parent, child)
|
|
78
90
|
else
|
|
79
91
|
line_to_parent(parent, child)
|
|
80
92
|
end
|
|
81
93
|
when "bar"
|
|
82
|
-
if
|
|
94
|
+
if forced
|
|
83
95
|
triangle_to_parent(parent, child)
|
|
84
96
|
else
|
|
85
97
|
line_to_parent(parent, child)
|
|
86
98
|
end
|
|
87
99
|
when "nothing", "none"
|
|
88
|
-
if
|
|
100
|
+
if forced
|
|
89
101
|
triangle_to_parent(parent, child)
|
|
90
102
|
elsif ETYPE_LEAF != child.type
|
|
91
103
|
line_to_parent(parent, child)
|
|
@@ -275,6 +287,7 @@ module RSyntaxTree
|
|
|
275
287
|
font_size: (@params[:fontsize] / FONT_SCALING).round,
|
|
276
288
|
color: @params[:color],
|
|
277
289
|
connector: @params[:leafstyle],
|
|
290
|
+
derivation: @params[:derivation] == true,
|
|
278
291
|
connector_height: @params[:vheight],
|
|
279
292
|
horizontal_spacing: @params[:hspacing] || 1.0,
|
|
280
293
|
line_width: @params[:linewidth],
|