rsyntaxtree 2.5.0 → 2.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f32301303f9acaffacc1783b5c66380187cb4394d559060f520bcbab64831680
4
- data.tar.gz: 80e8d7d6922d6025cbdef82a3efd64ee83ec8c60ff6f17f9607aefd4f7d8f782
3
+ metadata.gz: eb96318785756ecb47e67a06a809eb96bde5c4f63ceda113c13ba9bb221d032c
4
+ data.tar.gz: 48f083c0cec6539557959b0205e61c503f2aeeb68dea8b06ba3e5faf3b8dba6e
5
5
  SHA512:
6
- metadata.gz: cf8ebd19d64a05f09c88974c833ea87da9495b375f125e02a4ff34146b0ea2364115dc17cd08c3c9454d73f3a8e262a3a19f42c555f7af231901a19eb0813cad
7
- data.tar.gz: 0c5361ece29a29b4965d59bff8ef4f210a427c49643228c9afb92f8c2c012921752d53988898a720a1652a906c26a2b2f81de5b7876563747f35b3f5c41a4694
6
+ metadata.gz: fdd263b44eea3a6106bbdf504fe603d966e505d7f9b92251979fd373b5f50ddb15a686d87a3d9ec1ac6ce46d90a1153cfe79637063dabc3953542d66657d25c0
7
+ data.tar.gz: 37ba04230f1909bf30433f67ea51e283f19b9f45eccee865912eaee7fbb7d272bdd28d340f055d17716f92e02352ad5459cac9b29a8366dfc75e657d2fc1886d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.0] - 2026-11-12
4
+
5
+ ### Added
6
+ - `leafstyle: dashed` (`-l dashed`): the line down to each word dashed, every line between nodes solid
7
+ - Gallery: each example lists its leaf alignment, and its number can be found with the browser's search
8
+
9
+ ### Fixed
10
+ - Two movement paths that meet at one label no longer cross, and run on one level when nothing else is in the way
11
+
3
12
  ## [2.5.0] - 2026-11-04
4
13
 
5
14
  ### Added
data/CITATION.cff CHANGED
@@ -11,8 +11,8 @@ repository-code: "https://github.com/yohasebe/rsyntaxtree"
11
11
  # Concept DOI: always resolves to the latest archived version on Zenodo
12
12
  doi: "10.5281/zenodo.21916150"
13
13
  license: MIT
14
- version: 2.5.0
15
- date-released: 2026-11-04
14
+ version: 2.6.0
15
+ date-released: 2026-11-12
16
16
  keywords:
17
17
  - linguistics
18
18
  - syntax tree
data/bin/rsyntaxtree CHANGED
@@ -18,7 +18,7 @@ CONFIG_VALIDATORS = {
18
18
  outdir: ->(v) { FileTest.directory?(v) ? nil : "must be an existing directory path" },
19
19
  outfilename: ->(v) { v.is_a?(String) ? nil : "must be a string" },
20
20
  format: ->(v) { FORMATS.include?(v.to_s) || v.to_s == "lsif" ? nil : "must be #{FORMAT_LIST_MESSAGE}" },
21
- leafstyle: ->(v) { OPTION_VALUES[:leafstyle].include?(v.to_s) ? nil : "must be auto, triangle, bar, or nothing" },
21
+ leafstyle: ->(v) { OPTION_VALUES[:leafstyle].include?(v.to_s) ? nil : "must be auto, triangle, bar, dashed, or nothing" },
22
22
  fontstyle: ->(v) { OPTION_VALUES[:fontstyle].include?(v.to_s) ? nil : "must be sans, serif, cjk, or mono" },
23
23
  fontsize: ->(v) { v.is_a?(Integer) && v >= 6 && v <= 26 ? nil : "must be in the range of 6-26" },
24
24
  linewidth: ->(v) { v.is_a?(Numeric) && v >= 0.5 && v <= 3.0 ? nil : "must be in the range of 0.5-3.0" },
@@ -110,7 +110,7 @@ opts = Optimist.options do
110
110
  opt :outdir, "Output directory", default: "./", short: :o
111
111
  opt :outfilename, "Output file base name", default: "syntree", short: :u
112
112
  opt :format, "Output format: #{FORMAT_LIST_MESSAGE}", default: "png", short: :f
113
- opt :leafstyle, "visual style of tree leaves: auto, triangle, bar, or nothing", default: "auto", short: :l
113
+ opt :leafstyle, "visual style of tree leaves: auto, triangle, bar, dashed, or nothing", default: "auto", short: :l
114
114
  opt :fontstyle, "Font style: sans, serif, cjk, mono", default: "sans", short: :n
115
115
  opt :fontsize, "Size: 6-26", default: 16, short: :s
116
116
  opt :linewidth, "Size: 0.5-3.0", default: 1.0, short: :i
@@ -147,7 +147,7 @@ Optimist.die :direction, "must be one of: #{OPTION_VALUES[:direction].join(', ')
147
147
  Optimist.die :derivation, "must be one of: #{OPTION_VALUES[:derivation].join(', ')}" unless OPTION_VALUES[:derivation].include?(opts[:derivation])
148
148
  Optimist.die :hyphen, "must be markup or literal" unless OPTION_VALUES[:hyphen].include?(opts[:hyphen])
149
149
  Optimist.die :format, "must be #{FORMAT_LIST_MESSAGE}" unless FORMATS.include?(opts[:format])
150
- Optimist.die :leafstyle, "must be auto, triangle, bar, or nothing" unless OPTION_VALUES[:leafstyle].include?(opts[:leafstyle])
150
+ Optimist.die :leafstyle, "must be auto, triangle, bar, dashed, or nothing" unless OPTION_VALUES[:leafstyle].include?(opts[:leafstyle])
151
151
  Optimist.die :fontstyle, "must be sans, serif, cjk, or mono" unless OPTION_VALUES[:fontstyle].include?(opts[:fontstyle])
152
152
  Optimist.die :fontsize, "must be in the range of 6-26" unless opts[:fontsize] >= 6 && opts[:fontsize] <= 26
153
153
  Optimist.die :linewidth, "must be in the range of 0.5-3.0" unless opts[:linewidth] >= 0.5 && opts[:linewidth] <= 3.0
@@ -450,8 +450,10 @@ module RSyntaxTree
450
450
  end
451
451
 
452
452
  # `through` carries the connector chosen above a run of joints leaf
453
- # alignment inserted, down that run to the child it reaches.
454
- def draw_connector(id = 1, through = nil)
453
+ # alignment inserted, down that run to the child it reaches; `dashed`
454
+ # carries how far a dashed line has run so far, so that its dash goes on
455
+ # unbroken through each joint.
456
+ def draw_connector(id = 1, through = nil, dashed = 0)
455
457
  parent = @element_list.get_id(id)
456
458
  children = parent.children.map { |c| @element_list.get_id(c) }
457
459
 
@@ -466,23 +468,32 @@ module RSyntaxTree
466
468
 
467
469
  if through
468
470
  child = children.first
469
- line_to_parent(parent, child) if through == :line
470
- draw_connector(child.id, (through if child.inserted_joint))
471
+ draw_connector(child.id, (through if child.inserted_joint), dashed + draw_line(parent, child, through, dashed))
471
472
  return
472
473
  end
473
474
 
474
475
  chosen = children.map do |child|
475
476
  joints, target = past_inserted_joints(child)
476
477
  connector = connector_for(parent, target, children.size == 1)
478
+ connector = :dashed if connector == :line && @leafstyle == "dashed" && ETYPE_LEAF == target.type
479
+ run = 0
477
480
  case connector
478
481
  when :triangle then triangle_to_parent(parent, target, joints)
479
- when :line then line_to_parent(parent, child)
482
+ when :line, :dashed then run = draw_line(parent, child, connector, 0)
480
483
  else no_connector(parent, target, joints)
481
484
  end
482
- [child, connector]
485
+ [child, connector, run]
483
486
  end
484
487
 
485
- chosen.each { |child, connector| draw_connector(child.id, (connector if child.inserted_joint)) }
488
+ chosen.each { |child, connector, run| draw_connector(child.id, (connector if child.inserted_joint), run) }
489
+ end
490
+
491
+ # A line, dashed when it is one of the lines leafstyle `dashed` draws down
492
+ # to a word. Returns how long it is, for the piece below to carry on from.
493
+ def draw_line(parent, child, connector, dashed)
494
+ return 0 unless %i[line dashed].include?(connector)
495
+
496
+ line_to_parent(parent, child, dash_from: (dashed if connector == :dashed)).to_f
486
497
  end
487
498
 
488
499
  # Leaf alignment puts joints between a node and the child it reaches. They
@@ -507,7 +518,7 @@ module RSyntaxTree
507
518
  case @leafstyle
508
519
  when "auto"
509
520
  forced || child.contains_phrase ? :triangle : :line
510
- when "bar"
521
+ when "bar", "dashed"
511
522
  forced ? :triangle : :line
512
523
  when "nothing", "none"
513
524
  if forced
@@ -56,8 +56,11 @@ module RSyntaxTree
56
56
  @nodes << build_node(element)
57
57
  end
58
58
 
59
- def line_to_parent(parent, child)
60
- @edges << { from: parent.id, to: child.id, type: "dominance", connector: "line" }
59
+ def line_to_parent(parent, child, dash_from: nil)
60
+ edge = { from: parent.id, to: child.id, type: "dominance", connector: "line" }
61
+ edge[:dashed] = true if dash_from
62
+ @edges << edge
63
+ 0
61
64
  end
62
65
 
63
66
  # A triangle spans from the parent to its child, through any joints leaf
@@ -99,7 +99,7 @@ Options (the command line spells them --like-this):
99
99
  | fontsize | 6–26 |
100
100
  | color | modern, traditional, off, gray |
101
101
  | linewidth | every line's weight, 0.5–3.0, as a ratio of the font size |
102
- | leafstyle | auto, bar, nothing — what joins a node to its leaf |
102
+ | leafstyle | auto, bar, dashed, nothing — what joins a node to its leaf |
103
103
  | direction | ttb, ltr, btt |
104
104
  | mirror | flip the finished layout, for RTL scripts |
105
105
  | tidy | off, symmetric, low, medium, high — how tightly subtrees pack |
@@ -1,4 +1,4 @@
1
- RSyntaxTree examples: 81 trees, every one verified to draw.
1
+ RSyntaxTree examples: 85 trees, every one verified to draw.
2
2
 
3
3
  Each is the input behind a figure in the gallery at https://yohasebe.github.io/rsyntaxtree/examples.
4
4
  The settings line names the options the gallery records for that figure;
@@ -2488,7 +2488,7 @@ own(x,<>y)
2488
2488
  ]
2489
2489
  ```
2490
2490
 
2491
- ## 080 — Terminals levelled with pass-through joints
2491
+ ## 080 — Terminals levelled by hand with pass-through joints
2492
2492
 
2493
2493
  Category: General
2494
2494
  Settings: fontstyle=noto-serif tidy=low vheight=1.0
@@ -2639,3 +2639,58 @@ SYN\t#(HEAD\t|1|#)#)
2639
2639
  ---\
2640
2640
  AGR\t|2|#)#)#) [bark]]]
2641
2641
  ```
2642
+
2643
+ ## 087 — Terminals levelled automatically: whole subtrees drop
2644
+
2645
+ Category: General
2646
+ Settings: fontstyle=noto-serif leaf_align=subtrees polyline=on tidy=low vheight=1.0
2647
+
2648
+ ```
2649
+ [S
2650
+ [NP [D the] [N cat]]
2651
+ [VP
2652
+ [V sat]
2653
+ [PP [P on] [NP [D the] [N mat]]]]]
2654
+ ```
2655
+
2656
+ ## 088 — Wh-movement with the terminals levelled
2657
+
2658
+ Category: Generative Grammar
2659
+ Settings: fontstyle=noto-serif leaf_align=subtrees polyline=on tidy=low vheight=1.0
2660
+
2661
+ ```
2662
+ [CP
2663
+ [DP what+>1]
2664
+ [C' [C did]
2665
+ [TP
2666
+ [DP [D the] [NP [N child]]]
2667
+ [T' [T t]
2668
+ [VP [V find] [DP t+1]]]]]]
2669
+ ```
2670
+
2671
+ ## 089 — A Japanese sentence with its words on one row
2672
+
2673
+ Category: Multilingual
2674
+ Settings: fontstyle=noto-serif leaf_align=words leafstyle=dashed tidy=low vheight=1.0
2675
+
2676
+ ```
2677
+ [S
2678
+ [NP [N 太郎] [P が]]
2679
+ [VP
2680
+ [NP [AdjP [Adj 古い]] [NP [N 本] [P を]]]
2681
+ [V 読んだ]]]
2682
+ ```
2683
+
2684
+ ## 090 — A summarised phrase deep in the tree, with the terminals levelled
2685
+
2686
+ Category: General
2687
+ Settings: fontstyle=noto-serif leaf_align=words leafstyle=dashed tidy=low vheight=1.0
2688
+
2689
+ ```
2690
+ [TP
2691
+ [DP [D the]
2692
+ [NP [N man]
2693
+ [PP [P with] [^DP the hat]]]]
2694
+ [T' [T will]
2695
+ [VP [V give] [DP [D the] [NP [N book]]]]]]
2696
+ ```
@@ -57,7 +57,6 @@ module RSyntaxTree
57
57
  @polygon_styles = "<polygon style='fill: none; stroke: #{@col_connector}; stroke-width:#{@global[:stroke_normal]}; stroke-linejoin:round;stroke-linecap:round;' points='X1 Y1 X2 Y2 X3 Y3' />\n"
58
58
  @text_styles = "<text white-space='pre' alignment-baseline='text-top' style='fill: COLOR; stroke-width: 0; font-size: fontsize' x='X_VALUE' y='Y_VALUE'>CONTENT</text>\n"
59
59
  @tree_data = String.new
60
- @visited_x = {}
61
60
  @visited_y = {}
62
61
  @global = global
63
62
  end
@@ -387,6 +386,12 @@ module RSyntaxTree
387
386
  d << " L#{at.call(points.last)}"
388
387
  end
389
388
 
389
+ # The dash every dashed line in the figure is drawn with.
390
+ def dash_array
391
+ dash = @fontsize / 4.0
392
+ "stroke-dasharray='#{dash} #{dash}'"
393
+ end
394
+
390
395
  # One stroke rather than three lines meeting at right angles. Drawn this way
391
396
  # the dash pattern runs round each turn instead of restarting at it, and an
392
397
  # arrowhead is the end of the stroke rather than a marker on whichever line
@@ -402,8 +407,7 @@ module RSyntaxTree
402
407
  radius = [radius, leg.call(points[-2], points[-1]) - arrow_length].min if end_arrow
403
408
  radius = [radius, 0].max
404
409
 
405
- dash = @fontsize / 4.0
406
- dasharray = dashed ? "stroke-dasharray='#{dash} #{dash}' " : ""
410
+ dasharray = dashed ? "#{dash_array} " : ""
407
411
  markers = +""
408
412
  markers << "marker-start='url(#arrowStart)' " if start_arrow
409
413
  markers << "marker-end='url(#arrow)' " if end_arrow
@@ -444,13 +448,20 @@ module RSyntaxTree
444
448
 
445
449
  RAIL_SLACK = 1e-6
446
450
 
451
+ # How far apart two rails stand, and two path ends on one label.
452
+ def rail_spacing
453
+ @global[:h_gap_between_nodes] * 1.25
454
+ end
455
+
447
456
  # A rail that would run along one already drawn, over part of the same
448
- # span, is moved outward past it: two paths whose rails coincide read as
449
- # one line, and nothing tells which ends it joins.
457
+ # span or less than one spacing short of it, is moved outward past it: two
458
+ # paths whose rails coincide read as one line, and nothing tells which
459
+ # ends it joins. Rails a spacing or more apart end to end stay on one
460
+ # level, as two paths meeting at a label do once their ends are spread.
450
461
  #
451
462
  # Each move goes outward and past the rail in the way, so no rail can be in
452
- # the way twice and there are at most as many moves as rails. The distance
453
- # is compared with a little slack: a rail moved exactly one spacing past
463
+ # the way twice and there are at most as many moves as rails. Distances
464
+ # are compared with a little slack: a rail moved exactly one spacing past
454
465
  # another can measure a hair under it, and would be moved again forever.
455
466
  def clear_of_rails(at, span, spacing, sign)
456
467
  @rails ||= []
@@ -458,7 +469,7 @@ module RSyntaxTree
458
469
  close = spacing - RAIL_SLACK
459
470
  @rails.size.times do
460
471
  blocking = @rails.find do |r|
461
- r[:lo] <= hi + spacing && lo <= r[:hi] + spacing && (r[:at] - at).abs < close
472
+ r[:lo] < hi + close && lo < r[:hi] + close && (r[:at] - at).abs < close
462
473
  end
463
474
  break unless blocking
464
475
 
@@ -482,8 +493,38 @@ module RSyntaxTree
482
493
  b[:max_y] = [b[:max_y], ys.max].max
483
494
  end
484
495
 
485
- def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none)
486
- spacing = @global[:h_gap_between_nodes] * 1.25
496
+ # Path ends that stand in one line across the paths' run (on one label,
497
+ # or on labels straight above one another) are spread apart, each to the
498
+ # side its path heads for: a path going left leaves from the left and one
499
+ # going right from the right, so neither crosses the other's rail. Among
500
+ # ends heading the same way, the one that goes farther takes the outer
501
+ # place, since its rail runs outside. Left to right, the same holds with
502
+ # up and down in place of left and right. Neighbouring ends stand one
503
+ # rail spacing apart, the same distance that keeps two rails apart, or
504
+ # closer where that would take them past the narrowest label they share:
505
+ # an end stays on its label, and so off its neighbours and on the canvas.
506
+ def spread_shared_ends(paths)
507
+ along = @direction == "ltr" ? 1 : 0
508
+ ends = Hash.new { |h, k| h[k] = [] }
509
+ paths.each do |t|
510
+ s = [t[:x1], t[:y1]]
511
+ e = [t[:x2], t[:y2]]
512
+ ends[s[along].round] << [t, :s_offset, s[along], e[along], t[:across1]]
513
+ ends[e[along].round] << [t, :t_offset, e[along], s[along], t[:across2]]
514
+ end
515
+ ends.each_value do |shared|
516
+ gaps = shared.size - 1
517
+ step = gaps.zero? ? 0 : [rail_spacing, shared.map(&:last).min / gaps.to_f].min
518
+ shared.each_with_index
519
+ .sort_by { |(_, _, at, other, _), i| [other <=> at, -other, i] }
520
+ .each_with_index do |((t, key, _, _, _), _), place|
521
+ t[key] = (place - (gaps / 2.0)) * step
522
+ end
523
+ end
524
+ end
525
+
526
+ def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none, s_offset: 0, t_offset: 0)
527
+ spacing = rail_spacing
487
528
  min_bulge = @global[:height_connector_to_text]
488
529
  # An end that carries an arrowhead needs a run long enough for the head
489
530
  # and a stem before the turn. The rail clears the farther end by the
@@ -496,14 +537,6 @@ module RSyntaxTree
496
537
  # arc, and taken from a layout measure so it holds at every font size.
497
538
  corner_radius = min_bulge / 2.0
498
539
 
499
- # Centered offset for multiple lines at the same endpoint
500
- s_key = "#{s_x.round}"
501
- t_key = "#{t_x.round}"
502
- @visited_x[s_key] = (@visited_x[s_key] || 0) + 1
503
- @visited_x[t_key] = (@visited_x[t_key] || 0) + 1
504
- s_offset = ((@visited_x[s_key] - 1) - (@visited_x[s_key] - 1) / 2.0) * spacing
505
- t_offset = ((@visited_x[t_key] - 1) - (@visited_x[t_key] - 1) / 2.0) * spacing
506
-
507
540
  dashed = true if target_arrow == :none
508
541
 
509
542
  if @direction == "ltr"
@@ -545,8 +578,8 @@ module RSyntaxTree
545
578
  new_y = [new_y, s_y + (sign * arrow_run)].send(outward) if source_arrow
546
579
  new_y = [new_y, t_y + (sign * arrow_run)].send(outward) if target_arrow_drawn
547
580
 
548
- new_s_x = s_x - s_offset
549
- new_t_x = t_x - t_offset
581
+ new_s_x = s_x + s_offset
582
+ new_t_x = t_x + t_offset
550
583
  new_y = clear_of_rails(new_y, [new_s_x, new_t_x].minmax, spacing, sign)
551
584
 
552
585
  note_path_extent([[new_s_x, s_y], [new_s_x, new_y],
@@ -960,6 +993,14 @@ module RSyntaxTree
960
993
  # A movement path leaves a label on the side the words are on: below
961
994
  # it top to bottom, above it bottom to top.
962
995
  path_y = @direction == "btt" ? element.vertical_indent - @global[:height_connector_to_text] : y1
996
+ # How much of the label there is across the paths' run, for the ends
997
+ # that share it to be spread over: an even amount either side of the
998
+ # anchor, which left to right sits below the middle of the label.
999
+ across = if @direction == "ltr"
1000
+ [element.content_height - (@global[:height_connector_to_text] * 1.5), 0].max
1001
+ else
1002
+ element.content_width
1003
+ end
963
1004
  et = element.path
964
1005
  et.each do |tr|
965
1006
  if /\A-(>|<)?(\d+)\z/ =~ tr
@@ -974,20 +1015,20 @@ module RSyntaxTree
974
1015
  elsif /\A(?:>|<)(\d+)\z/ =~ tr
975
1016
  tr = $1
976
1017
  if path_pool_target[tr]
977
- path_pool_target[tr] << [x1, path_y]
1018
+ path_pool_target[tr] << [x1, path_y, across]
978
1019
  else
979
- path_pool_target[tr] = [[x1, path_y]]
1020
+ path_pool_target[tr] = [[x1, path_y, across]]
980
1021
  end
981
1022
  path_flags << tr
982
1023
  elsif path_pool_source[tr]
983
1024
  if path_pool_other[tr]
984
- path_pool_other[tr] << [x1, path_y]
1025
+ path_pool_other[tr] << [x1, path_y, across]
985
1026
  else
986
- path_pool_other[tr] = [[x1, path_y]]
1027
+ path_pool_other[tr] = [[x1, path_y, across]]
987
1028
  end
988
1029
  path_flags << tr
989
1030
  else
990
- path_pool_source[tr] = [x1, path_y]
1031
+ path_pool_source[tr] = [x1, path_y, across]
991
1032
  path_flags << tr
992
1033
  end
993
1034
  raise RSTError.new(+"Error: input text contains a path having more than two ends:\n > #{tr}", code: :path_multiple_ends, hint: "A path or line takes exactly two ends; check the numbers.", retryable: true) if path_flags.tally.any? { |_k, v| v > 2 } || line_flags.tally.any? { |_k, v| v > 2 }
@@ -1008,11 +1049,11 @@ module RSyntaxTree
1008
1049
  path_flags.delete(k)
1009
1050
  if (targets = path_pool_target[k])
1010
1051
  targets.each do |t|
1011
- paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :single }
1052
+ paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], across1: v[2], across2: t[2], arrow: :single }
1012
1053
  end
1013
1054
  elsif (others = path_pool_other[k])
1014
1055
  others.each do |t|
1015
- paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :none }
1056
+ paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], across1: v[2], across2: t[2], arrow: :none }
1016
1057
  end
1017
1058
  end
1018
1059
  end
@@ -1025,7 +1066,7 @@ module RSyntaxTree
1025
1066
  next if fst.nil?
1026
1067
 
1027
1068
  targets.each do |t|
1028
- paths << { x1: fst[0], y1: fst[1], x2: t[0], y2: t[1], arrow: :double }
1069
+ paths << { x1: fst[0], y1: fst[1], x2: t[0], y2: t[1], across1: fst[2], across2: t[2], arrow: :double }
1029
1070
  end
1030
1071
  end
1031
1072
 
@@ -1033,8 +1074,9 @@ module RSyntaxTree
1033
1074
  # than across its ends. Left to right, every rail already clears the
1034
1075
  # ones drawn before it, in the order written.
1035
1076
  paths = paths.each_with_index.sort_by { |t, i| [(t[:x1] - t[:x2]).abs, i] }.map(&:first) unless @direction == "ltr"
1077
+ spread_shared_ends(paths)
1036
1078
  paths.each do |t|
1037
- draw_a_path(t[:x1], t[:y1], t[:x2], t[:y2], t[:arrow])
1079
+ draw_a_path(t[:x1], t[:y1], t[:x2], t[:y2], t[:arrow], s_offset: t[:s_offset], t_offset: t[:t_offset])
1038
1080
  end
1039
1081
 
1040
1082
  line_pool.each do |_k, v|
@@ -1102,8 +1144,7 @@ module RSyntaxTree
1102
1144
  else
1103
1145
  ""
1104
1146
  end
1105
- dash = @fontsize / 4.0
1106
- dasharray = dashed ? "stroke-dasharray='#{dash} #{dash}'" : ""
1147
+ dasharray = dashed ? dash_array : ""
1107
1148
  swidth = bline ? @global[:stroke_bold] : @global[:stroke_normal]
1108
1149
 
1109
1150
  if s_arrow && t_arrow
@@ -1168,8 +1209,7 @@ module RSyntaxTree
1168
1209
  else
1169
1210
  ""
1170
1211
  end
1171
- dash = @fontsize / 4.0
1172
- dasharray = dashed ? "stroke-dasharray='#{dash} #{dash}'" : ""
1212
+ dasharray = dashed ? dash_array : ""
1173
1213
  swidth = bline ? @global[:stroke_bold] : @global[:stroke_normal]
1174
1214
 
1175
1215
  "<line x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}; stroke-linecap:round;' #{dasharray} #{string}/>"
@@ -1372,8 +1412,12 @@ module RSyntaxTree
1372
1412
  [child_y, parent_y]
1373
1413
  end
1374
1414
 
1375
- def line_to_parent(parent, child)
1376
- return if child.horizontal_indent.zero?
1415
+ # Dashed from +dash_from+ on, when given: a line through the joints leaf
1416
+ # alignment inserted is drawn one piece at a time, and each piece picks up
1417
+ # the dash where the one above it left off, so the run reads as one line.
1418
+ # Returns how long the line drawn is.
1419
+ def line_to_parent(parent, child, dash_from: nil)
1420
+ return 0 if child.horizontal_indent.zero?
1377
1421
 
1378
1422
  if @direction == "ltr"
1379
1423
  # LTR: parent's right side → child's left side
@@ -1391,19 +1435,9 @@ module RSyntaxTree
1391
1435
  mid_y1 = y1
1392
1436
  mid_x2 = mid_x1
1393
1437
  mid_y2 = y2
1394
- @tree_data += @polyline_styles.sub(/CHIX/, x1.to_s)
1395
- .sub(/CHIY/, y1.to_s)
1396
- .sub(/MIDX1/, mid_x1.to_s)
1397
- .sub(/MIDY1/, mid_y1.to_s)
1398
- .sub(/MIDX2/, mid_x2.to_s)
1399
- .sub(/MIDY2/, mid_y2.to_s)
1400
- .sub(/PARX/, x2.to_s)
1401
- .sub(/PARY/, y2.to_s)
1438
+ points = [[x1, y1], [mid_x1, mid_y1], [mid_x2, mid_y2], [x2, y2]]
1402
1439
  else
1403
- line_data = @line_styles.sub(/X1/, x1.to_s)
1404
- line_data = line_data.sub(/Y1/, y1.to_s)
1405
- line_data = line_data.sub(/X2/, x2.to_s)
1406
- @tree_data += line_data.sub(/Y2/, y2.to_s)
1440
+ points = [[x1, y1], [x2, y2]]
1407
1441
  end
1408
1442
  else
1409
1443
  # TTB: parent's bottom → child's top
@@ -1418,25 +1452,38 @@ module RSyntaxTree
1418
1452
  mid_x2 = par_x
1419
1453
  mid_y2 = mid_y1
1420
1454
 
1421
- @tree_data += @polyline_styles.sub(/CHIX/, chi_x.to_s)
1422
- .sub(/CHIY/, chi_y.to_s)
1423
- .sub(/MIDX1/, mid_x1.to_s)
1424
- .sub(/MIDY1/, mid_y1.to_s)
1425
- .sub(/MIDX2/, mid_x2.to_s)
1426
- .sub(/MIDY2/, mid_y2.to_s)
1427
- .sub(/PARX/, par_x.to_s)
1428
- .sub(/PARY/, par_y.to_s)
1455
+ points = [[chi_x, chi_y], [mid_x1, mid_y1], [mid_x2, mid_y2], [par_x, par_y]]
1429
1456
  else
1430
1457
  x1 = child.horizontal_indent + child.content_width / 2.0
1431
1458
  x2 = parent.horizontal_indent + parent.content_width / 2.0
1432
1459
  y1, y2 = connector_edges(child, parent)
1433
-
1434
- line_data = @line_styles.sub(/X1/, x1.to_s)
1435
- line_data = line_data.sub(/Y1/, y1.to_s)
1436
- line_data = line_data.sub(/X2/, x2.to_s)
1437
- @tree_data += line_data.sub(/Y2/, y2.to_s)
1460
+ points = [[x1, y1], [x2, y2]]
1438
1461
  end
1439
1462
  end
1463
+ emit_connector(points, dash_from)
1464
+ end
1465
+
1466
+ # +points+ run from the child to the parent. A dashed line is laid the
1467
+ # other way, from the parent down, so that its dash carries on into the
1468
+ # piece below it.
1469
+ def emit_connector(points, dash_from)
1470
+ dash = ""
1471
+ if dash_from
1472
+ points = points.reverse
1473
+ dash = " #{dash_array} stroke-dashoffset='#{dash_from.round(3)}'"
1474
+ end
1475
+ if points.size == 2
1476
+ (x1, y1), (x2, y2) = points
1477
+ @tree_data += @line_styles.sub(/X1/, x1.to_s).sub(/Y1/, y1.to_s)
1478
+ .sub(/X2/, x2.to_s).sub(/Y2/, y2.to_s).sub(" />", "#{dash} />")
1479
+ else
1480
+ (chix, chiy), (mx1, my1), (mx2, my2), (parx, pary) = points
1481
+ @tree_data += @polyline_styles.sub(/CHIX/, chix.to_s).sub(/CHIY/, chiy.to_s)
1482
+ .sub(/MIDX1/, mx1.to_s).sub(/MIDY1/, my1.to_s)
1483
+ .sub(/MIDX2/, mx2.to_s).sub(/MIDY2/, my2.to_s)
1484
+ .sub(/PARX/, parx.to_s).sub(/PARY/, pary.to_s).sub(" />", "#{dash} />")
1485
+ end
1486
+ points.each_cons(2).sum { |(ax, ay), (bx, by)| Math.hypot(bx - ax, by - ay) }
1440
1487
  end
1441
1488
 
1442
1489
  # Draw a triangle between child/parent elements
@@ -189,7 +189,7 @@ module RSyntaxTree
189
189
 
190
190
  label = extract_label(element)
191
191
  escaped_label = escape_latex(label)
192
- head = "#{escaped_label}#{region_option(element)}"
192
+ head = "#{escaped_label}#{leaf_edge_option(element)}#{region_option(element)}"
193
193
 
194
194
  children = element.children
195
195
  indent_str = " " * indent
@@ -202,6 +202,17 @@ module RSyntaxTree
202
202
  end
203
203
  end
204
204
 
205
+ # The edge down to a word is dashed when leafstyle asks for it, every
206
+ # piece of it: leaf alignment runs it through joints, and forest gives
207
+ # each joint the edge from the node above. Added to the edge style the
208
+ # tree sets, not in place of it, so the width holds.
209
+ def leaf_edge_option(element)
210
+ return "" unless @params[:leafstyle] == "dashed"
211
+
212
+ element = @element_list.get_id(element.children.first) while element.inserted_joint
213
+ element.type == ETYPE_LEAF ? ", edge+=dashed" : ""
214
+ end
215
+
205
216
  # forest node option that paints a semi-transparent plane behind the whole
206
217
  # subtree governed by +element+ (region shade). Uses forest's "fit to=tree"
207
218
  # on the background layer so the plane sits below the tree. Empty string
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSyntaxTree
4
- VERSION = "2.5.0"
4
+ VERSION = "2.6.0"
5
5
  end
data/lib/rsyntaxtree.rb CHANGED
@@ -55,7 +55,7 @@ OPTION_VALUES = {
55
55
  # an alias until 3.0. It collided with a code-intelligence format of the
56
56
  # same initials, and claimed an interchange nobody else had implemented.
57
57
  format: FORMATS + ["lsif"],
58
- leafstyle: %w[auto triangle bar nothing],
58
+ leafstyle: %w[auto triangle bar dashed nothing],
59
59
  fontstyle: ["sans", "serif", "cjk", "mono", "noto-sans", "noto-serif", "noto-sans-mono", "cjk zenhei"],
60
60
  color: %w[modern traditional gray grey off none on true false],
61
61
  tidy: %w[off symmetric low medium high on compact true false],
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: 2.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoichiro Hasebe