rsyntaxtree 1.14.0 → 2.1.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: 965fe8dae50ad6fe7907850239f395fda6a5186654a81d36b89400ebe52108f2
4
- data.tar.gz: 4a183744f4d05b63efcbd1e9aedb6a4bf060654fde9dfc62abaeb20e6a38ac8a
3
+ metadata.gz: 231de78b2e34e37f4a5bbccd641839e8cf5ae1babc2d8d4362d86dcf782d28b4
4
+ data.tar.gz: a4d04e71e6587592540400aa9a7e23d9dadae0d19912078d5d6cc5ebd65c117d
5
5
  SHA512:
6
- metadata.gz: ee4fca1e926ad6886aaca041468f61dc0a596eeffa107c4ee3a82576f2ac7ac53eddcdc0ee586fc5f4408fc809188dba1279eb0ff8083892ef1b598aea830ca1
7
- data.tar.gz: 19173b80e3c2376a9f25d6ea72604bc5774194af7eccfc2b1158dfdf75939ed5d700f654b8122ecf3df99242036686666266ed829710fbe22d94bbea965ae91b
6
+ metadata.gz: f97797690b6d8e0ad8404c00a10c061c89d2213e582915a33fc9d739305d5fb10cdd0afde6bffe0b16f504b5d5854d7dcb0249f50a22b10599638fb22180aed3
7
+ data.tar.gz: 7f5814cc9bf0875b21982cf6c143397d467fb900a7973c8f6213934387ba4f80e0605ddccec63f02bc7bacc6d182a9da1bda3237f909414ba4c64f63a1da1d12
data/CHANGELOG.md CHANGED
@@ -1,5 +1,136 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.0] - 2026-08-29
4
+
5
+ ### Changed
6
+ - The JSON output's format name is `json`: `-f json`, writing `syntree.json`.
7
+ The old name `lsif` collided with an established code-intelligence format
8
+ of the same initials, and called itself an interchange format when only
9
+ one tool had ever written or read it. It remains an accepted alias — the
10
+ CLI says so on stderr — until 3.0. The document now identifies itself in a
11
+ top-level `format` object (`name: "rsyntaxtree-json"`, schema 0.4.0) in
12
+ place of the `lsif` key.
13
+
14
+ ### Added
15
+ - A feature matrix in the JSON output carries its rows: cells split on the
16
+ tab stops, rules as rows of their own, and a matrix inside a cell
17
+ recursing. It used to arrive as an empty segment marked "matrix", its
18
+ attribute-value pairs recoverable only by re-parsing the raw label.
19
+
20
+ ## [2.0.0] - 2026-08-28
21
+
22
+ The removals announced in 1.10.0 and 1.12.0, and the fixes that came out of
23
+ looking at what was left. Every figure in the gallery draws exactly as it did.
24
+
25
+ ### Removed
26
+ - JPG and GIF output. JPEG blurs line art and GIF has nothing to offer that
27
+ PNG does not do better, and they were the only reason for the RMagick
28
+ dependency. `-f jpg` and `-f gif` are now refused the way any unknown
29
+ format is. Use `-f png`.
30
+ - The RMagick dependency, and with it ImageMagick. A machine that installs
31
+ this gem no longer needs ImageMagick's headers to build one, and the gem
32
+ now asks for four libraries instead of five.
33
+ - The deprecated aliases, which a major release is the moment to drop:
34
+ `symmetrize` and its `-y` flag (say `tidy: symmetric`), `tidy_spacing`
35
+ (say `hspacing`), the undocumented `tidy_nest`, and `nothing`'s
36
+ undocumented spelling `none` in `leafstyle`. Each is refused by name and
37
+ told what replaced it. An unknown option is passed over in silence, since
38
+ a caller may hand its own parameters through, and a removed one read that
39
+ way would have handed back a different figure without a word.
40
+ - `symmetrize` from the LSIF `meta.source.params` block. The layout scale it
41
+ duplicated, `tidy`, is written there already.
42
+
43
+ ### Fixed
44
+ - A triangle drawn bottom to top pointed the wrong way. The shape spans the
45
+ child's text and points at the parent, and which way that is depends on
46
+ which of the two sits higher — the question the connectors ask and the
47
+ triangle did not. Bottom to top the figure folded through itself: the base
48
+ struck through the leaf and the apex through the node. No gallery figure
49
+ carries one, because every bottom-to-top example is a derivation, which
50
+ draws rules instead of connectors.
51
+ - Every label now keeps the same air above it as below it. The connector
52
+ endpoints were measured from the layout box, whose edges sit unevenly
53
+ around the ink, so every figure carried a little more space under each
54
+ label than over it — visibly once a figure was tilted. The enclosures
55
+ were built with symmetric clearances all along; the plain labels now get
56
+ what the boxes always had. Nearly every gallery figure changes by a few
57
+ pixels of line length; no label, no position and no size moves.
58
+ - A movement rail could reach past the right edge of a left-to-right
59
+ canvas and be cut off at it: the rail grows the canvas as it routes, and
60
+ the width it grew — unlike the height — was then recomputed from the
61
+ elements alone. Four pixels at the default spacing, twenty-four at
62
+ hspacing 0.5.
63
+ - A rule written `---` (or `===` for a double rule) was refused inside a
64
+ `#( … #)` matrix, though it was drawn in every other kind of label. Two
65
+ hyphens and four were accepted there and three were not — a distinction
66
+ nothing could explain. The matrix grammar had no rule for it and the
67
+ matrix renderer dropped every row that was not text.
68
+ - The bracket of a matrix that followed something in the same cell was drawn
69
+ on top of it: `AGR |1| [ … ]`, which is how a shared value is written, put
70
+ the right edge of the tag and the left edge of the bracket on the same
71
+ coordinate exactly. A block now keeps as much room in front of it as the
72
+ bracket keeps inside it.
73
+ - In a left-to-right tree, `hspacing` and `vheight` both moved the width and
74
+ nothing moved the height. The layout replaces the gap that separates
75
+ sisters with one of its own and was not scaling it by `hspacing`, so a
76
+ left-to-right figure could be made wider but never shorter.
77
+ - `storoke-width` in the style of every text element of every SVG. Renderers
78
+ ignored the misspelling, so nothing ever looked wrong.
79
+
80
+ ### Added
81
+ - `shear`: the drawn figure tilts by the given angle (degrees, positive
82
+ leaning the top to the right) and lies on a plane drawn behind it, so the
83
+ lean reads as a surface seen at an angle rather than as a mistake. The
84
+ whole picture shears as one affine piece — layout untouched, region
85
+ shades coming out parallelograms on their own, and nothing able to newly
86
+ touch or cross. The plane is fill without an edge — a region shade is
87
+ bounded because it marks one part of a figure off from the rest, and a
88
+ line round the whole drawing would read as a frame. `shear_plane` turns
89
+ it off or gives it a colour, and a transparent background takes it away
90
+ for you: a clear background is asked for in order to lay the figure over
91
+ something else, and a sheet under it is what would defeat that.
92
+ TikZ refuses a sheared figure the way it refuses a derivation.
93
+ - `vmargin`: the clearance between a label and its connectors, the same
94
+ above and below, measured from the ink its row actually contains — a row
95
+ of capitals pays nothing for descenders nobody wrote, and the ends of a
96
+ row's connectors stay level. From 0.1 to 1.0; at nothing at all a node's
97
+ box stops holding a label that draws its own shape.
98
+ - Three gallery examples, in two new categories: the Indo-European family
99
+ tree (Historical Linguistics), where the region shade groups branches
100
+ rather than marking a c-command domain; type-driven semantic composition
101
+ (Formal Semantics); and a tree of typed feature structures with structure
102
+ sharing (Formal Grammar), which is the figure the matrix rule was missing.
103
+
104
+ ### Changed
105
+ - The documents name every option. Four were missing from the one-page
106
+ reference and one from the README, because the test that reads the
107
+ documents built its roll-call from the tables of listed and numeric
108
+ values — and so never asked after the options that take on or off. It
109
+ reads the roll-call from the defaults now, and the README is one of the
110
+ documents it reads.
111
+ - Three tests were added for the shapes of defect this release fixed: one
112
+ asks every feature in the index to read the same in a label, in a matrix
113
+ and in a matrix inside a matrix; one reads the drawn shapes back out of
114
+ the SVG and asks whether any two of them touch; and one asks a
115
+ left-to-right tree to answer to `hspacing` in its height.
116
+ - The sweep draws two more figures — one with a triangle, one with a
117
+ movement rail — and moves the direction, `vmargin` and `shear` among its
118
+ settings, and asks of every triangle that it have an area. Each of the two
119
+ defects above lived in a figure the sweep did not draw, at a direction it
120
+ did not set: the settings alone could not have found either, and a
121
+ triangle folded flat passes every other check there is.
122
+ - The manuals say what TikZ output does with what it cannot draw: it drops
123
+ it rather than refusing, so a label that is a matrix arrives with its
124
+ cells run together on one line.
125
+
126
+ ### Migration
127
+ - `format: "jpg"` / `"gif"` → `"png"`.
128
+ - `symmetrize: "on"` → `tidy: "symmetric"`; `tidy_spacing: n` → `hspacing: n`;
129
+ `leafstyle: "none"` → `"nothing"`. Each has been the documented spelling
130
+ since 1.12.0.
131
+ - Everything else — every other option, and every piece of notation — is
132
+ unchanged.
133
+
3
134
  ## [1.14.0] - 2026-08-27
4
135
 
5
136
  ### Added
@@ -513,12 +644,40 @@ RMagick dependency.
513
644
 
514
645
  ## [1.3.1] - 2024-10-24
515
646
 
647
+ - Square brackets can be escaped with `\[` and `\]`
516
648
  - Bug fixes and improvements
517
649
 
518
650
  ## [1.3.0] - 2024-08-27
519
651
 
520
652
  - TikZ/forest LaTeX output support
521
653
 
522
- ## Previous versions
523
-
524
- See commit history for earlier changes.
654
+ ## Earlier releases (2018–2023)
655
+
656
+ Before this file was kept, the web interface carried the release notes. Moved
657
+ here so the history has one home.
658
+
659
+ - 2023-09 — Cross-hatch rendering issue fixed
660
+ - 2023-02 — PDF download; improved line edges; `Line width` option;
661
+ `Traditional` colour option; `Hide connectors` option; extra connectors
662
+ between nodes
663
+ - 2023-01 — `Noto Sans Mono` font option
664
+ - 2022-11 — `¥` accepted in place of the backslash for escapes; documentation
665
+ in Japanese
666
+ - 2022-06 — RSyntaxTree on the Web: the UI as a source package, runnable
667
+ locally with Docker
668
+ - 2022-02 — `Polyline connector` option
669
+ - 2022-01 — comprehensive rewrite; path drawing (non-directional, directional,
670
+ bidirectional); more flexible text markup; new markup patterns (newlines,
671
+ brackets, horizontal lines, boxed text); triangles in every connector mode;
672
+ improved error display and image zoom in the UI
673
+ - 2021-12 — subscript `_text_` and superscript `__text__`; `\n` inside
674
+ terminal nodes
675
+ - 2021-11 — label-less terminal nodes (`[A [B] [C]]`); auto bracket close;
676
+ math symbol rendering; better image quality; some decoration symbols altered
677
+ - 2019-11 — SVG download error fixed
678
+ - 2019-04 — SVG colour on/off fixed
679
+ - 2018-06 — whitespace in labels with `<>` (`Modal<>AUX`)
680
+ - 2018-03 — `V spacing` option (then called Connector height); syntax
681
+ highlighting of the input; text styles (italic, bold) and decoration
682
+ (underline, overline, line-through); refined site design
683
+ - 2018-02 — margins around the image; better editor (Ace)
data/README.md CHANGED
@@ -69,7 +69,7 @@ Connect nodes with lines or arrows:
69
69
 
70
70
  ### Multiple Output Formats
71
71
 
72
- Generate trees in PNG, SVG, PDF, or LSIF (JSON) format. (JPG and GIF are deprecated and will be removed in 2.0.)
72
+ Generate trees in PNG, SVG, PDF, TikZ, or JSON format the last carrying the laid-out tree as structured data.
73
73
 
74
74
  ## Web Interface
75
75
 
@@ -170,18 +170,25 @@ Usage:
170
170
  | Option | Description | Default |
171
171
  |--------|-------------|---------|
172
172
  | `-o, --outdir` | Output directory | `./` |
173
- | `-f, --format` | Output format: png, svg, pdf, lsif, tikz (jpg and gif are deprecated) | `png` |
173
+ | `-f, --format` | Output format: png, svg, pdf, json, tikz | `png` |
174
174
  | `-l, --leafstyle` | Leaf style: auto, triangle, bar, nothing | `auto` |
175
175
  | `-n, --fontstyle` | Font style: sans, serif, cjk, mono | `sans` |
176
176
  | `-s, --fontsize` | Font size: 6-26 | `16` |
177
177
  | `-i, --linewidth` | Line width relative to the font size: 0.5-3.0 (`1` = 5% of the font size) | `1.0` |
178
178
  | `-c, --color` | Color mode: modern, traditional, gray (black text, grey lines), off | `modern` |
179
179
  | `-p, --polyline` | Polyline connectors: on, off | `off` |
180
- | `-d, --direction` | Tree layout direction: ttb, ltr | `ttb` |
180
+ | `--hide-default-connectors` | Draw no parent-to-child lines, leaving only the ones written with `+-n`: on, off | `off` |
181
+ | `-d, --direction` | Tree layout direction: ttb, ltr, btt | `ttb` |
181
182
  | `--tidy` | Layout scale: off, symmetric, low, medium, high | `off` |
182
- | `--hspacing` | Horizontal spacing factor, all layout modes (0.5-3.0) | `1.0` |
183
+ | `--hspacing` | How far apart sisters sit, all layout modes (0.5-3.0) | `1.0` |
184
+ | `-v, --vheight` | How far apart levels sit (0.5-5.0) | `2.0` |
185
+ | `--derivation` | Join each node to its daughters with one rule drawn across them: on, off | `off` |
183
186
  | `--hyphen` | How a hyphen reads: markup (`-underline-`) or literal | `markup` |
184
187
  | `-m, --mirror` | Flip the tree horizontally (RTL convention): on, off | `off` |
188
+ | `--shear` | Tilt the drawn figure, in degrees (-45 to 45, positive leans the top right) | `0` |
189
+ | `--shear-plane` | The plane behind a sheared figure: on, off, or a colour | `on` |
190
+ | `--vmargin` | Clearance between a label and its connectors (0.1-1.0, same above and below) | `0.4` |
191
+ | `-r, --transparent` | Leave the background clear instead of painting it white: on, off | `off` |
185
192
  | `--validate` | Check the input without drawing: a JSON diagnosis on stdout, the verdict in the exit code | |
186
193
  | `--notation` | Print a short reference for the notation | |
187
194
 
@@ -215,10 +222,11 @@ a model writing it does better with the reference in front of it than without.
215
222
  [web interface](https://yohasebe.com/rsyntaxtree) or on your own machine —
216
223
  nothing needs installing. Give the model one of these to read:
217
224
 
218
- - [The brief reference](https://yohasebe.github.io/rsyntaxtree/llms.txt), about
219
- a page, leading with the characters that already mean something.
225
+ - [The reference, on one page](https://yohasebe.github.io/rsyntaxtree/notation.txt):
226
+ the characters that already mean something, then every feature at a line
227
+ each, then the options. About 6 KB.
220
228
  - [Everything in one file](https://yohasebe.github.io/rsyntaxtree/llms-full.txt):
221
- the reference, the manual and all the gallery examples, 78 KB.
229
+ the reference, the manual and all the gallery examples, about 85 KB.
222
230
 
223
231
  **If the model can run a shell**, it can draw and then look at what it drew,
224
232
  which is the part that lets it correct itself:
data/bin/rsyntaxtree CHANGED
@@ -9,7 +9,7 @@ require_relative '../lib/rsyntaxtree'
9
9
  require_relative '../lib/rsyntaxtree/utils'
10
10
  require_relative '../lib/rsyntaxtree/format_converter'
11
11
 
12
- # "png, jpg, gif, pdf, svg, lsif, or tikz" — built once from the library's
12
+ # "png, pdf, svg, json, or tikz" — built once from the library's
13
13
  # own list so the three places that name the formats cannot drift apart.
14
14
  FORMAT_LIST_MESSAGE = FORMATS.join(", ").sub(/, ([^,]+)\z/, ', or \1')
15
15
 
@@ -17,21 +17,22 @@ FORMAT_LIST_MESSAGE = FORMATS.join(", ").sub(/, ([^,]+)\z/, ', or \1')
17
17
  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
- format: ->(v) { FORMATS.include?(v.to_s) ? nil : "must be #{FORMAT_LIST_MESSAGE}" },
20
+ format: ->(v) { FORMATS.include?(v.to_s) || v.to_s == "lsif" ? nil : "must be #{FORMAT_LIST_MESSAGE}" },
21
21
  leafstyle: ->(v) { OPTION_VALUES[:leafstyle].include?(v.to_s) ? nil : "must be auto, triangle, bar, 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" },
25
25
  vheight: ->(v) { v.is_a?(Numeric) && v >= 0.5 && v <= 5.0 ? nil : "must be in the range of 0.5-5.0" },
26
26
  color: ->(v) { OPTION_VALUES[:color].include?(v.to_s) ? nil : "must be modern, traditional, gray, or off" },
27
- symmetrize: ->(v) { /\A(on|off|true|false)\z/ =~ v.to_s ? nil : "must be on or off" },
28
27
  transparent: ->(v) { /\A(on|off|true|false)\z/ =~ v.to_s ? nil : "must be on or off" },
29
28
  polyline: ->(v) { /\A(on|off|true|false)\z/ =~ v.to_s ? nil : "must be on or off" },
30
29
  hide_default_connectors: ->(v) { /\A(on|off|true|false)\z/ =~ v.to_s ? nil : "must be on or off" },
31
30
  mirror: ->(v) { /\A(on|off|true|false)\z/ =~ v.to_s ? nil : "must be on or off" },
31
+ shear: ->(v) { v.is_a?(Numeric) && v >= -45 && v <= 45 ? nil : "must be in the range of -45-45" },
32
+ vmargin: ->(v) { v.is_a?(Numeric) && v >= 0.1 && v <= 1.0 ? nil : "must be in the range of 0.1-1.0" },
33
+ shear_plane: ->(v) { v.to_s =~ /\A(on|off)\z/ || RSyntaxTree::COLOR_NAMES.include?(v.to_s.downcase) || v.to_s =~ /\A#(\h{3}|\h{6})\z/ ? nil : "must be on, off, or a colour" },
32
34
  tidy: ->(v) { OPTION_VALUES[:tidy].include?(v.to_s) ? nil : "must be off, symmetric, low, medium, or high" },
33
35
  hspacing: ->(v) { v.is_a?(Numeric) && v >= 0.5 && v <= 3.0 ? nil : "must be in the range of 0.5-3.0" },
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
36
  direction: ->(v) { OPTION_VALUES[:direction].include?(v.to_s) ? nil : "must be one of: #{OPTION_VALUES[:direction].join(', ')}" },
36
37
  hyphen: ->(v) { OPTION_VALUES[:hyphen].include?(v.to_s) ? nil : "must be markup or literal" }
37
38
  }.freeze
@@ -114,14 +115,15 @@ opts = Optimist.options do
114
115
  opt :linewidth, "Size: 0.5-3.0", default: 1.0, short: :i
115
116
  opt :vheight, "Connector Height: 0.5-5.0", default: 2.0, short: :v
116
117
  opt :color, "Color text and bars: modern, traditional, gray, or off", default: "modern", short: :c
117
- opt :symmetrize, "DEPRECATED alias of --tidy symmetric: on or off", default: "off", short: :y
118
118
  opt :transparent, "Make background transparent: on or off", default: "off", short: :r
119
119
  opt :polyline, "draw polyline connectors: on or off", default: "off", short: :p
120
120
  opt :hide_default_connectors, "make default connectors transparent: on or off", default: "off", short: :none
121
121
  opt :mirror, "flip the tree horizontally (RTL linguistics convention: first word at the right edge): on or off", default: "off"
122
+ opt :shear, "Tilt the drawn figure, in degrees: -45 to 45, positive leaning the top to the right", default: 0.0, short: :none
123
+ opt :shear_plane, "The plane behind a sheared figure: on, off, a colour name, or a hex colour", default: "on", short: :none
124
+ opt :vmargin, "Clearance between a label and its connectors, 0.1-1.0", default: 0.4, short: :none
122
125
  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
126
  opt :hspacing, "Horizontal spacing factor (counterpart of vheight): 0.5-3.0", default: 1.0, short: :none
124
- opt :tidy_spacing, "DEPRECATED alias of --hspacing: 0.5-3.0", default: 1.0, short: :none
125
127
  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
128
  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
127
129
  opt :hyphen, "How a hyphen in a label reads: markup (-underline-) or literal (a hyphen; \-underline\- then underlines)", default: "markup", short: :none
@@ -131,6 +133,13 @@ opts = Optimist.options do
131
133
  opt :help, "This is a custom help message", short: :h
132
134
  end
133
135
 
136
+ # The name the JSON format wore from 1.11.0 to 2.0.0. Accepted until 3.0,
137
+ # and said out loud, the way JPG and GIF were seen out.
138
+ if opts[:format] == "lsif"
139
+ warn "Warning: the format name 'lsif' is now 'json'. The old name will be removed in RSyntaxTree 3.0."
140
+ opts[:format] = "json"
141
+ end
142
+
134
143
  Optimist.die :outdir, "must be an exsting directory path" unless FileTest.directory?(opts[:outdir])
135
144
  Optimist.die :direction, "must be one of: #{OPTION_VALUES[:direction].join(', ')}" unless OPTION_VALUES[:direction].include?(opts[:direction])
136
145
  Optimist.die :derivation, "must be one of: #{OPTION_VALUES[:derivation].join(', ')}" unless OPTION_VALUES[:derivation].include?(opts[:derivation])
@@ -140,8 +149,10 @@ Optimist.die :leafstyle, "must be auto, triangle, bar, or nothing" unless OPTION
140
149
  Optimist.die :fontstyle, "must be sans, serif, cjk, or mono" unless OPTION_VALUES[:fontstyle].include?(opts[:fontstyle])
141
150
  Optimist.die :fontsize, "must be in the range of 6-26" unless opts[:fontsize] >= 6 && opts[:fontsize] <= 26
142
151
  Optimist.die :linewidth, "must be in the range of 0.5-3.0" unless opts[:linewidth] >= 0.5 && opts[:linewidth] <= 3.0
152
+ Optimist.die :shear, "must be in the range of -45-45" unless opts[:shear] >= -45 && opts[:shear] <= 45
153
+ Optimist.die :vmargin, "must be in the range of 0.1-1.0" unless opts[:vmargin] >= 0.1 && opts[:vmargin] <= 1.0
154
+ Optimist.die :shear_plane, "must be on, off, or a colour" unless /\A(on|off)\z/ =~ opts[:shear_plane] || RSyntaxTree::COLOR_NAMES.include?(opts[:shear_plane].downcase) || /\A#(\h{3}|\h{6})\z/ =~ opts[:shear_plane]
143
155
  Optimist.die :color, "must be either modern, traditional, gray, or off" unless OPTION_VALUES[:color].include?(opts[:color])
144
- Optimist.die :symmetrize, "must be either on or off" unless /\A(on|off|true|false)\z/ =~ opts[:symmetrize]
145
156
  Optimist.die :vheight, "must be in the range of 0.5-5.0" if opts[:vheight] < 0.5 || opts[:vheight] > 5.0
146
157
  Optimist.die :transparent, "must be either on or off" unless /\A(on|off|true|false)\z/ =~ opts[:transparent]
147
158
  Optimist.die :polyline, "must be either on or off" unless /\A(on|off|true|false)\z/ =~ opts[:polyline]
@@ -149,7 +160,6 @@ Optimist.die :hide_default_connectors, "must be either on or off" unless /\A(on|
149
160
  Optimist.die :mirror, "must be either on or off" unless /\A(on|off|true|false)\z/ =~ opts[:mirror]
150
161
  Optimist.die :tidy, "must be off, symmetric, low, medium, or high" unless OPTION_VALUES[:tidy].include?(opts[:tidy])
151
162
  Optimist.die :hspacing, "must be in the range of 0.5-3.0" if opts[:hspacing] < 0.5 || opts[:hspacing] > 3.0
152
- Optimist.die :tidy_spacing, "must be in the range of 0.5-3.0" if opts[:tidy_spacing] < 0.5 || opts[:tidy_spacing] > 3.0
153
163
 
154
164
  # Merge config file options with CLI options (CLI takes precedence)
155
165
  string_opts = {}
@@ -170,10 +180,6 @@ opts.each do |key, value|
170
180
  end
171
181
  end
172
182
 
173
- if %w[jpg gif].include?(string_opts[:format])
174
- warn "Warning: #{string_opts[:format].upcase} output is deprecated and will be removed in RSyntaxTree 2.0. Use PNG instead."
175
- end
176
-
177
183
  # These print and exit, so they answer before the input is read. Reading
178
184
  # first meant waiting on a pipe that never closes when stdin is not a
179
185
  # terminal, which is how they are reached from a script.
@@ -14,7 +14,6 @@ module RSyntaxTree
14
14
  def initialize(element_list, params, global)
15
15
  @global = global
16
16
  @element_list = element_list
17
- @symmetrize = params[:symmetrize] == true
18
17
  @direction = params[:direction] || "ttb"
19
18
 
20
19
  case params[:color]
@@ -72,13 +71,10 @@ module RSyntaxTree
72
71
  # "off" | "low" (packing, strict leaf positions) | "medium" (packing
73
72
  # with cross-row tucking as long as no two leaves swap left-right
74
73
  # order) | "high" (free tucking; leaf order kept per row only).
75
- # Legacy inputs upgrade into the scale: tidy_nest: on lifts "low" to
76
- # "high", and the old standalone symmetrize: on lifts "off" to
77
- # "symmetric". When tidy packing is active the symmetric layout is
78
- # meaningless, so packing wins over a legacy symmetrize flag.
74
+ # One scale, and nothing beside it: the standalone symmetrize and
75
+ # tidy_nest flags that used to lift a mode into another were removed
76
+ # in 2.0, so what is asked for is what is drawn.
79
77
  tidy_mode = params[:tidy].to_s
80
- tidy_mode = "high" if tidy_mode == "low" && params[:tidy_nest] == true
81
- tidy_mode = "symmetric" if tidy_mode == "off" && @symmetrize
82
78
  @tidy = %w[low medium high].include?(tidy_mode)
83
79
  # :none — leaf spans may not overlap at all (strict positions)
84
80
  # :ordered — spans may overlap, but leaf centers keep their order
@@ -966,7 +962,13 @@ module RSyntaxTree
966
962
  # is disproportionately large relative to the swapped content dimensions.
967
963
  # Use height_connector_to_text / 2 (= font_height / 4) for tight
968
964
  # vertical packing proportional to the font size.
969
- @global[:h_gap_between_nodes] = @global[:height_connector_to_text] / 2
965
+ #
966
+ # Scaled by hspacing, like the gap it replaces. Without it the two
967
+ # spacing options both pushed the width in this direction — hspacing
968
+ # only through the margins it is left in — and nothing at all decided
969
+ # how far apart siblings sat, so a left-to-right tree could be made
970
+ # wider but never shorter.
971
+ @global[:h_gap_between_nodes] = @global[:height_connector_to_text] / 2 * @global[:hspacing]
970
972
 
971
973
  # In LTR, height_connector becomes horizontal depth between levels.
972
974
  # After content swap, content_height = original content_width (small),
@@ -1090,7 +1092,14 @@ module RSyntaxTree
1090
1092
  max_x = @rule_name_edge if @rule_name_edge.to_f > max_x
1091
1093
  width = max_x + @global[:h_gap_between_nodes]
1092
1094
  height = max_y
1095
+ # A movement path routes outside the tree — below it top-to-bottom,
1096
+ # beyond it to the right left-to-right — and grows @height or @width as
1097
+ # it goes. The height has always been read back here; the width was
1098
+ # computed from the elements alone and then overwrote what the path had
1099
+ # grown, so a left-to-right path could reach past the canvas and be cut
1100
+ # off at its edge.
1093
1101
  height = @height if @height > height
1102
+ width = @width if @width > width
1094
1103
  { height: height, width: width }
1095
1104
  end
1096
1105
  end
@@ -13,7 +13,7 @@ require_relative "color_names"
13
13
 
14
14
  module RSyntaxTree
15
15
  class Element
16
- attr_accessor :rule_name, :label_with_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
16
+ attr_accessor :ink_top, :ink_bottom, :rule_name, :label_with_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
17
 
18
18
  # names_a_rule says the content is a mother's label rather than a leaf's
19
19
  # text. Only a mother has a step under it for a name to sit beside, and a
@@ -309,6 +309,10 @@ module RSyntaxTree
309
309
  row_holds_text = content[:elements].any? do |e|
310
310
  (e[:decoration] & [:box, :circle, :bar]).empty? && !e[:text].to_s.strip.empty?
311
311
  end
312
+ # True until something is drawn in the current column. A tabstop
313
+ # opens the next one, so a block that starts a column is at the
314
+ # start of a cell however many columns came before it.
315
+ cell_start = true
312
316
  content[:elements].each do |e|
313
317
  # A nested matrix is measured by the same code one level down, and
314
318
  # reports the size of the block it will occupy in this row: its own
@@ -317,7 +321,18 @@ module RSyntaxTree
317
321
  inner = measure_lines(e[:matrix], nested: true)
318
322
  e[:matrix_width] = inner[:width]
319
323
  e[:matrix_height] = inner[:height]
320
- e[:width] = inner[:width] + matrix_bracket_room * 2
324
+ # A block that opens a cell needs nothing in front of it: the
325
+ # column it starts is already held clear. One that follows
326
+ # something in the same cell — the tag on a shared value, as in
327
+ # AGR |1| [ ... ] — was drawn with its bracket on the ink before
328
+ # it, because a run is spaced for the glyph that comes next and
329
+ # a bracket is not a glyph.
330
+ # As much room in front of the bracket as the bracket keeps
331
+ # inside it, so the two sides of it look alike. Half as much
332
+ # left it looking crowded from the outside.
333
+ e[:matrix_lead] = cell_start ? 0 : matrix_bracket_room
334
+ cell_start = false
335
+ e[:width] = inner[:width] + matrix_bracket_room * 2 + e[:matrix_lead]
321
336
  # Two separate allowances. The block is padded inside its own
322
337
  # brackets, above and below, and that padding is part of the row.
323
338
  # The gap that keeps the block clear of the rows either side is
@@ -335,6 +350,14 @@ module RSyntaxTree
335
350
  next
336
351
  end
337
352
 
353
+ # A tabstop opens the next column; anything else puts ink in the
354
+ # one being filled.
355
+ if e[:decoration].to_a.include?(:tabstop)
356
+ cell_start = true
357
+ elsif !e[:text].to_s.empty?
358
+ cell_start = false
359
+ end
360
+
338
361
  text = e[:text]
339
362
  # Handle escaped square brackets
340
363
  text = text.gsub('\\[', '[')
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #==========================
4
- # lsif_graph.rb
4
+ # json_graph.rb
5
5
  #==========================
6
6
  #
7
7
  # Generates LSIF (Linguistic Structure Interchange Format) JSON output.
@@ -12,7 +12,7 @@ require_relative 'base_graph'
12
12
  require_relative 'utils'
13
13
 
14
14
  module RSyntaxTree
15
- class LsifGraph < BaseGraph
15
+ class JSONGraph < BaseGraph
16
16
  attr_accessor :width, :height
17
17
 
18
18
  def initialize(element_list, params, global)
@@ -30,7 +30,7 @@ module RSyntaxTree
30
30
  @global = global
31
31
  end
32
32
 
33
- def lsif_data
33
+ def json_data
34
34
  metrics = parse_list
35
35
  @width = metrics[:width] + @global[:h_gap_between_nodes] * 2
36
36
  @height = metrics[:height] + @global[:height_connector_to_text] / 2
@@ -241,14 +241,7 @@ module RSyntaxTree
241
241
  element.content.each do |l|
242
242
  next unless l[:type] == :text
243
243
 
244
- segments = l[:elements].map do |e|
245
- next if e[:decoration].include?(:whitespace) && e[:text].strip.empty?
246
-
247
- {
248
- text: e[:text].gsub(WHITESPACE_BLOCK, " ").gsub('&#62;', '>').gsub('&#60;', '<'),
249
- decorations: e[:decoration].map(&:to_s).reject { |d| d == "whitespace" }
250
- }
251
- end.compact
244
+ segments = l[:elements].map { |e| build_segment(e) }.compact
252
245
  lines << { segments: segments } unless segments.empty?
253
246
  end
254
247
 
@@ -258,6 +251,43 @@ module RSyntaxTree
258
251
  }
259
252
  end
260
253
 
254
+ def build_segment(run)
255
+ return nil if run[:decoration].include?(:whitespace) && run[:text].strip.empty?
256
+
257
+ segment = {
258
+ text: run[:text].gsub(WHITESPACE_BLOCK, " ").gsub('&#62;', '>').gsub('&#60;', '<'),
259
+ decorations: run[:decoration].map(&:to_s).reject { |d| d == "whitespace" }
260
+ }
261
+ # A feature matrix used to reach this document as an empty segment
262
+ # marked "matrix", its attribute-value pairs recoverable only by
263
+ # re-parsing the raw label — which is no way to score whether a model
264
+ # wrote CASE nom where CASE nom was wanted. The rows come through now:
265
+ # cells split on the tab stops, rules kept as rows of their own, a
266
+ # matrix inside a cell recursing.
267
+ segment[:matrix] = { rows: build_matrix_rows(run[:matrix]) } if run[:decoration].include?(:matrix) && run[:matrix]
268
+ segment
269
+ end
270
+
271
+ def build_matrix_rows(matrix_lines)
272
+ matrix_lines.filter_map do |line|
273
+ case line[:type]
274
+ when :border then { rule: "single" }
275
+ when :bborder then { rule: "double" }
276
+ when :text
277
+ cells = [[]]
278
+ line[:elements].each do |run|
279
+ if run[:decoration].include?(:tabstop)
280
+ cells << []
281
+ else
282
+ segment = build_segment(run)
283
+ cells.last << segment if segment
284
+ end
285
+ end
286
+ { cells: cells }
287
+ end
288
+ end
289
+ end
290
+
261
291
  def map_enclosure(enclosure)
262
292
  case enclosure
263
293
  when :brackets then "brackets"
@@ -273,8 +303,14 @@ module RSyntaxTree
273
303
 
274
304
  def build_json
275
305
  data = {
276
- lsif: {
277
- version: "0.3.0",
306
+ # What this document is, said by a field rather than by the key, so a
307
+ # reader checks ["format"]["name"] and the key never has to change
308
+ # again. Documents up to schema 0.3.0 said it with a top-level "lsif"
309
+ # key instead — a name that collided with the code-intelligence
310
+ # format of the same initials.
311
+ format: {
312
+ name: "rsyntaxtree-json",
313
+ version: "0.4.0",
278
314
  generator: "rsyntaxtree #{RSyntaxTree::VERSION}",
279
315
  level: "rendered"
280
316
  },
@@ -291,12 +327,18 @@ module RSyntaxTree
291
327
  connector_height: @params[:vheight],
292
328
  horizontal_spacing: @params[:hspacing] || 1.0,
293
329
  line_width: @params[:linewidth],
294
- # The layout scale the figure was drawn on. symmetrize is kept
295
- # for readers of older files; tidy: "symmetric" supersedes it.
330
+ # The layout scale the figure was drawn on. Files written
331
+ # before 2.0 carry a separate symmetrize flag beside it; the
332
+ # scale is what it meant, and it is all that is written now.
296
333
  tidy: @params[:tidy] || "off",
297
- symmetrize: @params[:symmetrize],
298
334
  mirror: @params[:mirror] == true,
299
335
  direction: @params[:direction] || "ttb",
336
+ # Degrees, positive leaning the top right. The geometry below
337
+ # is the unsheared layout — the shear is applied at render — so
338
+ # a reader who wants the drawn look applies it, and one who
339
+ # wants the layout ignores it.
340
+ shear: (@params[:shear] || 0).to_f,
341
+ vmargin: (@params[:vmargin] || 0.4).to_f,
300
342
  # Which of the two readings of a hyphen the input was parsed
301
343
  # under. The input is recorded verbatim above, and the same
302
344
  # string means different things under the two, so a reader
@@ -5,8 +5,13 @@ require 'parslet'
5
5
  class MarkupParser < Parslet::Parser
6
6
  rule(:cr) { str('\\n') }
7
7
  rule(:eof) { any.absent? }
8
- rule(:border) { match('[^\-]').absent? >> str('-').repeat(3).as(:border) >> (eof | cr) }
9
- rule(:bborder) { match('[^=]').absent? >> str('=').repeat(3).as(:bborder) >> (eof | cr) }
8
+ rule(:border) { rule_line >> (eof | cr) }
9
+ rule(:bborder) { double_rule_line >> (eof | cr) }
10
+ # The rule itself, without what ends the line. A matrix ends its rows on
11
+ # its own closing delimiter as well as on a line break, so it supplies its
12
+ # own terminator; folding one in here would have the two consume it twice.
13
+ rule(:rule_line) { match('[^\-]').absent? >> str('-').repeat(3).as(:border) }
14
+ rule(:double_rule_line) { match('[^=]').absent? >> str('=').repeat(3).as(:bborder) }
10
15
 
11
16
  rule(:brectangle) { str('###') }
12
17
  rule(:rectangle) { str('##') }
@@ -79,7 +84,7 @@ class MarkupParser < Parslet::Parser
79
84
  # bare brackets would be read as tree structure and bare parentheses appear
80
85
  # in labels too often to claim.
81
86
  rule(:matrix) { str('#(') >> matrix_line.repeat(1).as(:matrix) >> str('#)') }
82
- rule(:matrix_line) { matrix_markup.repeat(1).as(:line) >> (cr | str('#)').present?) }
87
+ rule(:matrix_line) { (rule_line | double_rule_line | matrix_markup.repeat(1).as(:line)) >> (cr | str('#)').present?) }
83
88
  rule(:matrix_markup) { (matrix | tabstop | matrix_text | decoration | shape | bstroke) }
84
89
  # Text inside a matrix stops at the closing delimiter as well.
85
90
  rule(:matrix_text) { (escaped | (str('#)').absent? >> non_escaped)).repeat(1).as(:text) }
@@ -13,7 +13,10 @@ figure that was meant.
13
13
  - `<>` is one space and `<3>` is three, everywhere — never angle brackets.
14
14
  Where linguistics wants the brackets themselves, write ⟨ and ⟩ (U+27E8/9):
15
15
  SPR\t⟨<>NP<>⟩, 'hand⟨SUBJ,OBJ⟩'. A label written <NP> is refused with this
16
- advice; a stray <3> simply draws three spaces.
16
+ advice; a stray <3> simply draws three spaces. No text face carries these
17
+ two characters, so whatever the machine falls back to draws them — they
18
+ will not quite match the letters beside them, and how far off they look
19
+ depends on the fonts installed.
17
20
  - A pair of hyphens underlines what stands between them: well-made-word draws
18
21
  "made" underlined. (A lone hyphen is refused with a hint.) Write `\-` for a
19
22
  hyphen — V\-bar, HEAD\-DTR — or set the hyphen option to literal.
@@ -84,7 +87,7 @@ Options (the command line spells them --like-this):
84
87
 
85
88
  | option | what it decides |
86
89
  |------------|-----------------|
87
- | format | png, svg, pdf, gif, jpg, tikz, lsif |
90
+ | format | png, svg, pdf, tikz, json (the tree as structured data) |
88
91
  | fontstyle | sans, serif, mono, cjk |
89
92
  | fontsize | 6–26 |
90
93
  | color | modern, traditional, off, gray |
@@ -93,10 +96,20 @@ Options (the command line spells them --like-this):
93
96
  | direction | ttb, ltr, btt |
94
97
  | mirror | flip the finished layout, for RTL scripts |
95
98
  | tidy | off, symmetric, low, medium, high — how tightly subtrees pack |
96
- | hspacing | horizontal spacing factor, 0.5–3.0 (tidy_spacing is its old name) |
97
- | vheight | vertical spacing factor, 0.5–5.0 |
99
+ | hspacing | how far apart sisters sit, 0.5–3.0 |
100
+ | vheight | how far apart levels sit, 0.5–5.0 |
101
+ | polyline | draw each connector as two right angles rather than a slanted line |
102
+ | hide_default_connectors | draw no parent-to-child lines, leaving only the ones written with +-n — how a lattice or a network is drawn |
103
+ | transparent | leave the background clear instead of painting it white |
98
104
  | derivation | one rule across the daughters, categorial-grammar style |
99
105
  | hyphen | markup (hyphens underline) or literal (hyphens are hyphens) |
106
+ | shear | tilt the drawn figure: degrees, −45 to 45, positive leans the top right |
107
+ | shear_plane | the plane behind a sheared figure: on (grey), off, or a colour; never drawn on a transparent background |
108
+ | vmargin | clearance between a label and its connectors, 0.1–1.0, the same above and below |
109
+
110
+ hspacing and vheight are named for the top-to-bottom layout, and follow the
111
+ tree rather than the page: with direction ltr, sisters stack downwards and
112
+ hspacing is what separates them, while vheight runs across.
100
113
 
101
114
  The full manual explains each of these with figures, and every example in the
102
115
  gallery is written out beside the figure it draws: