rsyntaxtree 1.11.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 +49 -0
- data/bin/rsyntaxtree +21 -8
- data/lib/rsyntaxtree/base_graph.rb +151 -0
- data/lib/rsyntaxtree/element.rb +99 -19
- data/lib/rsyntaxtree/lsif_graph.rb +12 -0
- data/lib/rsyntaxtree/notation_core.md +7 -7
- data/lib/rsyntaxtree/notation_examples.md +2487 -0
- 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 +46 -7
- data/rsyntaxtree.gemspec +11 -3
- metadata +2 -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 -103
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,54 @@
|
|
|
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
|
+
|
|
22
|
+
## [1.12.0] - 2026-08
|
|
23
|
+
|
|
24
|
+
Everything needed to write this notation is now available as text, built from
|
|
25
|
+
the files the tool itself reads, and a refusal says where that text is.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `--examples` prints every published example with the options it was drawn
|
|
29
|
+
with. The gallery examples are checked against the parser that draws them,
|
|
30
|
+
so each one is known to be accepted.
|
|
31
|
+
- The site carries the same material as plain text for a reader that can fetch
|
|
32
|
+
a URL but cannot run a command: `llms.txt` and `llms-full.txt`, the second
|
|
33
|
+
holding the reference, the manual and every example in one file. Both are
|
|
34
|
+
generated from their sources, and a test fails if they fall behind.
|
|
35
|
+
- A refusal names where the notation is written down, once, alongside the
|
|
36
|
+
cause and the fix. A hint repairs the mistake in front of it and says
|
|
37
|
+
nothing about the rest, which is not enough for a caller that was guessing.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- The angle bracket the reference recommends is U+27E8 and U+27E9, which it
|
|
41
|
+
had always named while writing U+3008 and U+3009 in its own examples. The
|
|
42
|
+
East Asian pair draws a full em wide, wider than a capital, and had spread
|
|
43
|
+
through the gallery and into the repair that corrects ASCII brackets. Both
|
|
44
|
+
pairs still parse; documents written before this are unaffected.
|
|
45
|
+
- Two feature-structure figures are redrawn at the narrower bracket.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- `--notation` and `--examples` answer before the input is read. Reading first
|
|
49
|
+
meant waiting on a pipe that never closes when stdin is not a terminal,
|
|
50
|
+
which is how a script reaches them.
|
|
51
|
+
|
|
3
52
|
## [1.11.0] - 2026-08
|
|
4
53
|
|
|
5
54
|
Every input the tool accepts now draws, and every input it refuses says why.
|
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,15 +122,18 @@ 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
|
|
130
|
+
opt :examples, "Print every gallery example, with the options each needs, to stdout and exit", short: :none
|
|
129
131
|
opt :help, "This is a custom help message", short: :h
|
|
130
132
|
end
|
|
131
133
|
|
|
132
134
|
Optimist.die :outdir, "must be an exsting directory path" unless FileTest.directory?(opts[:outdir])
|
|
133
|
-
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])
|
|
134
137
|
Optimist.die :hyphen, "must be markup or literal" unless OPTION_VALUES[:hyphen].include?(opts[:hyphen])
|
|
135
138
|
Optimist.die :format, "must be #{FORMAT_LIST_MESSAGE}" unless FORMATS.include?(opts[:format])
|
|
136
139
|
Optimist.die :leafstyle, "must be auto, triangle, bar, or nothing" unless OPTION_VALUES[:leafstyle].include?(opts[:leafstyle])
|
|
@@ -171,6 +174,21 @@ if %w[jpg gif].include?(string_opts[:format])
|
|
|
171
174
|
warn "Warning: #{string_opts[:format].upcase} output is deprecated and will be removed in RSyntaxTree 2.0. Use PNG instead."
|
|
172
175
|
end
|
|
173
176
|
|
|
177
|
+
# These print and exit, so they answer before the input is read. Reading
|
|
178
|
+
# first meant waiting on a pipe that never closes when stdin is not a
|
|
179
|
+
# terminal, which is how they are reached from a script.
|
|
180
|
+
if opts[:notation]
|
|
181
|
+
puts File.read(File.expand_path("../lib/rsyntaxtree/notation_core.md", __dir__))
|
|
182
|
+
exit 0
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# The examples are the reference's counterpart: the rules in brief there, and
|
|
186
|
+
# here every published tree with the options it was drawn with.
|
|
187
|
+
if opts[:examples]
|
|
188
|
+
puts File.read(File.expand_path("../lib/rsyntaxtree/notation_examples.md", __dir__))
|
|
189
|
+
exit 0
|
|
190
|
+
end
|
|
191
|
+
|
|
174
192
|
# An argument names a file only if it really is one: File.exist? on an
|
|
175
193
|
# expanded empty string answers for the current directory, and reading that
|
|
176
194
|
# as the tree ended the run with a Ruby error.
|
|
@@ -187,11 +205,6 @@ data = if ARGV[0] && !ARGV[0].empty? && File.file?(File.expand_path(ARGV[0]))
|
|
|
187
205
|
# Auto-convert Penn TreeBank format to bracket notation
|
|
188
206
|
data = RSyntaxTree::FormatConverter.to_bracket(data) if data
|
|
189
207
|
|
|
190
|
-
if opts[:notation]
|
|
191
|
-
puts File.read(File.expand_path("../lib/rsyntaxtree/notation_core.md", __dir__))
|
|
192
|
-
exit 0
|
|
193
|
-
end
|
|
194
|
-
|
|
195
208
|
# Validate without drawing or writing anything: the diagnosis goes to stdout
|
|
196
209
|
# as JSON and the exit code carries the verdict.
|
|
197
210
|
if opts[:validate]
|
|
@@ -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
|
|
|
@@ -133,8 +161,8 @@ module RSyntaxTree
|
|
|
133
161
|
->(s) { s.sub(/\A%?@(?:#[0-9a-zA-Z]+|[a-zA-Z]+)?:/, "") },
|
|
134
162
|
"A color is @name: with a CSS color name, or @#rgb: / @#rrggbb: with 3 or 6 hex digits (e.g. @blue:VP, @#3af:VP)."],
|
|
135
163
|
[:angle_brackets,
|
|
136
|
-
->(s) { s.gsub(/(?<!\\)<([^<>]*[^<>\d][^<>]*)>/) { "
|
|
137
|
-
"'<' and '>' mark whitespace here, not a list. Write the angle bracket characters themselves:
|
|
164
|
+
->(s) { s.gsub(/(?<!\\)<([^<>]*[^<>\d][^<>]*)>/) { "⟨#{$1}⟩" } },
|
|
165
|
+
"'<' and '>' mark whitespace here, not a list. Write the angle bracket characters themselves: ⟨NP⟩, 'hand⟨SUBJ,OBJ⟩'."],
|
|
138
166
|
[:bare_hyphen,
|
|
139
167
|
->(s) { Element.escape_hyphens(s) },
|
|
140
168
|
"A hyphen opens an underline. Escape it (e.g. f\\-structure, V\\-bar) or set the hyphen option to literal."],
|
|
@@ -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.
|
|
@@ -175,7 +213,7 @@ module RSyntaxTree
|
|
|
175
213
|
# away, one of them starting the split and the next ending it.
|
|
176
214
|
if label.strip.empty?
|
|
177
215
|
return details.merge(code: :label_split,
|
|
178
|
-
hint: "Raw spaces left this label empty. Write a space inside a label as <> (e.g.
|
|
216
|
+
hint: "Raw spaces left this label empty. Write a space inside a label as <> (e.g. ⟨<>NP<>⟩).",
|
|
179
217
|
retryable: true)
|
|
180
218
|
end
|
|
181
219
|
|
|
@@ -195,7 +233,7 @@ module RSyntaxTree
|
|
|
195
233
|
# a hint that admits no one cause was found rather than naming a wrong
|
|
196
234
|
# one. retryable: false is for what no rewriting reaches.
|
|
197
235
|
details.merge(code: :invalid_markup,
|
|
198
|
-
hint: "No single cause fits, which usually means more than one mistake in this label. Check that *, _, =, ~, |, { and #(...#) are paired, that hyphens are escaped, and that
|
|
236
|
+
hint: "No single cause fits, which usually means more than one mistake in this label. Check that *, _, =, ~, |, { and #(...#) are paired, that hyphens are escaped, and that ⟨ ⟩ are the angle bracket characters.",
|
|
199
237
|
retryable: true)
|
|
200
238
|
end
|
|
201
239
|
|
|
@@ -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],
|
|
@@ -16,10 +16,10 @@ Within a label:
|
|
|
16
16
|
|
|
17
17
|
Four traps, because these characters already mean something:
|
|
18
18
|
- `<` and `>` are never angle brackets in this notation: `<>` is one space,
|
|
19
|
-
`<3>` is three. Wherever linguistics uses angle brackets — a list
|
|
20
|
-
an argument structure like 'hand
|
|
21
|
-
characters
|
|
22
|
-
SPR\t
|
|
19
|
+
`<3>` is three. Wherever linguistics uses angle brackets — a list ⟨NP⟩,
|
|
20
|
+
an argument structure like 'hand⟨SUBJ,OBJ⟩', anything — write the
|
|
21
|
+
characters ⟨ and ⟩ themselves (U+27E8 and U+27E9):
|
|
22
|
+
SPR\t⟨<>NP<>⟩ PRED\t'hand⟨SUBJ,OBJ⟩'
|
|
23
23
|
- `-` opens and closes an underline, so a bare hyphen in ANY word is an
|
|
24
24
|
error: V-bar, f-structure, HEAD-DTR. Write `V'`, or escape the hyphen:
|
|
25
25
|
`V\-bar`, `f\-structure`, `HEAD\-DTR`.
|
|
@@ -35,14 +35,14 @@ An attribute-value matrix is columns plus an enclosure:
|
|
|
35
35
|
|
|
36
36
|
[#*word*\
|
|
37
37
|
HEAD\t*verb*\
|
|
38
|
-
SPR\t
|
|
39
|
-
COMPS\t
|
|
38
|
+
SPR\t⟨<>NP<>⟩\
|
|
39
|
+
COMPS\t⟨<>⟩
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
A value can be another matrix. Nest it with `#( ... #)`, never with square
|
|
43
43
|
brackets — those are read as tree structure:
|
|
44
44
|
|
|
45
|
-
[#PRED\t'hand
|
|
45
|
+
[#PRED\t'hand⟨SUBJ,OBJ⟩'\
|
|
46
46
|
TENSE\tpast\
|
|
47
47
|
SUBJ\t#(PRED\t'David'#)
|
|
48
48
|
]
|