rsyntaxtree 2.0.0 → 2.2.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/.dockerignore +5 -0
- data/CHANGELOG.md +35 -0
- data/CODE_OF_CONDUCT.md +85 -0
- data/CONTRIBUTING.md +89 -0
- data/README.md +12 -18
- data/bin/rsyntaxtree +12 -8
- data/lib/rsyntaxtree/{lsif_graph.rb → json_graph.rb} +49 -13
- data/lib/rsyntaxtree/notation_core.md +1 -1
- data/lib/rsyntaxtree/notation_examples.md +42 -16
- data/lib/rsyntaxtree/string_parser.rb +74 -8
- data/lib/rsyntaxtree/version.rb +1 -1
- data/lib/rsyntaxtree.rb +132 -21
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe4cd7a8b28ffa968f99ff228e3a182a64d420e0405cd27e120b5f4ff02fe48e
|
|
4
|
+
data.tar.gz: 23a64934f448707de2b55cad4c206d65fab18aa7d6c1cd3aab0b1871efd9be1e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2dfd53e10a62187649fb3d7285fa38189ef25cf56d6af7ceea27fba3cc7358b1f7d528db6466607061120fab61a2ff7bd9b84fa0b989e4313faaac306af42235
|
|
7
|
+
data.tar.gz: 7399787ea179bdd17910fa9695cc292a18cb5b8e9b3948e943bf32fd52fd05f89e6ec0e2792138197d56c839b9960bab678489a976590667eca9b4dd80679029
|
data/.dockerignore
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.0] - 2026-09-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Validation reports every error of a stage, not just the first one found.
|
|
7
|
+
An input with three bad labels lists all three in the `errors` array of
|
|
8
|
+
the JSON diagnosis, so one round of fixes covers them. The stages are
|
|
9
|
+
ordered — options, bracket structure, labels, whole-tree checks — and a
|
|
10
|
+
mistake stops the later stages, with a `note` saying that fixing what is
|
|
11
|
+
listed may reveal more; nothing is reported whose appearance is an
|
|
12
|
+
artifact of an earlier mistake. Duplicate failures collapse to one entry
|
|
13
|
+
and the list stops at twenty. The schema is unchanged
|
|
14
|
+
(`rsyntaxtree.error/1`): the same array, more of it.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `RSGenerator.diagnose(text, options)` returns the full diagnosis as a
|
|
18
|
+
hash — what `--validate` prints — for programmatic callers.
|
|
19
|
+
`check_data` keeps its contract of raising the first error.
|
|
20
|
+
|
|
21
|
+
## [2.1.0] - 2026-08-29
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- The JSON output's format name is `json`: `-f json`, writing `syntree.json`.
|
|
25
|
+
The old name `lsif` collided with an established code-intelligence format
|
|
26
|
+
of the same initials, and called itself an interchange format when only
|
|
27
|
+
one tool had ever written or read it. It remains an accepted alias — the
|
|
28
|
+
CLI says so on stderr — until 3.0. The document now identifies itself in a
|
|
29
|
+
top-level `format` object (`name: "rsyntaxtree-json"`, schema 0.4.0) in
|
|
30
|
+
place of the `lsif` key.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- A feature matrix in the JSON output carries its rows: cells split on the
|
|
34
|
+
tab stops, rules as rows of their own, and a matrix inside a cell
|
|
35
|
+
recursing. It used to arrive as an empty segment marked "matrix", its
|
|
36
|
+
attribute-value pairs recoverable only by re-parsing the raw label.
|
|
37
|
+
|
|
3
38
|
## [2.0.0] - 2026-08-28
|
|
4
39
|
|
|
5
40
|
The removals announced in 1.10.0 and 1.12.0, and the fixes that came out of
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
# Contributor Covenant Code of Conduct
|
|
3
|
+
|
|
4
|
+
## Our Pledge
|
|
5
|
+
|
|
6
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
|
7
|
+
|
|
8
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
|
9
|
+
|
|
10
|
+
## Our Standards
|
|
11
|
+
|
|
12
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
|
13
|
+
|
|
14
|
+
* Demonstrating empathy and kindness toward other people
|
|
15
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
16
|
+
* Giving and gracefully accepting constructive feedback
|
|
17
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
|
18
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
|
19
|
+
|
|
20
|
+
Examples of unacceptable behavior include:
|
|
21
|
+
|
|
22
|
+
* The use of sexualized language or imagery, and sexual attention or advances of any kind
|
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
24
|
+
* Public or private harassment
|
|
25
|
+
* Publishing others' private information, such as a physical or email address, without their explicit permission
|
|
26
|
+
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
27
|
+
|
|
28
|
+
## Enforcement Responsibilities
|
|
29
|
+
|
|
30
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
|
31
|
+
|
|
32
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
|
33
|
+
|
|
34
|
+
## Scope
|
|
35
|
+
|
|
36
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
|
37
|
+
|
|
38
|
+
## Enforcement
|
|
39
|
+
|
|
40
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at <yohasebe@gmail.com>. All complaints will be reviewed and investigated promptly and fairly.
|
|
41
|
+
|
|
42
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
|
43
|
+
|
|
44
|
+
## Enforcement Guidelines
|
|
45
|
+
|
|
46
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
|
47
|
+
|
|
48
|
+
### 1. Correction
|
|
49
|
+
|
|
50
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
|
51
|
+
|
|
52
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
|
53
|
+
|
|
54
|
+
### 2. Warning
|
|
55
|
+
|
|
56
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
|
57
|
+
|
|
58
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
|
59
|
+
|
|
60
|
+
### 3. Temporary Ban
|
|
61
|
+
|
|
62
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
|
63
|
+
|
|
64
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
|
65
|
+
|
|
66
|
+
### 4. Permanent Ban
|
|
67
|
+
|
|
68
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
|
69
|
+
|
|
70
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
|
71
|
+
|
|
72
|
+
## Attribution
|
|
73
|
+
|
|
74
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
75
|
+
|
|
76
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
|
77
|
+
|
|
78
|
+
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
|
|
79
|
+
|
|
80
|
+
[homepage]: https://www.contributor-covenant.org
|
|
81
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
82
|
+
[Mozilla CoC]: https://github.com/mozilla/diversity
|
|
83
|
+
[FAQ]: https://www.contributor-covenant.org/faq
|
|
84
|
+
[translations]: https://www.contributor-covenant.org/translations
|
|
85
|
+
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Contributing to RSyntaxTree
|
|
2
|
+
|
|
3
|
+
This page explains how to report a problem, propose a change, and get a
|
|
4
|
+
development environment running.
|
|
5
|
+
|
|
6
|
+
## Reporting bugs
|
|
7
|
+
|
|
8
|
+
Open an issue at <https://github.com/yohasebe/rsyntaxtree/issues> with:
|
|
9
|
+
|
|
10
|
+
- the input text (the bracket notation) and the options you used
|
|
11
|
+
- what you expected and what you got — for drawing problems, attach the image
|
|
12
|
+
- your platform and how you run RSyntaxTree (gem, Docker, or the
|
|
13
|
+
[web interface](https://yohasebe.com/rsyntaxtree))
|
|
14
|
+
|
|
15
|
+
If the input fails to draw, include the output of:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
rsyntaxtree --validate "your input here"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It reports the problem in a structured form that is usually enough to
|
|
22
|
+
diagnose the issue.
|
|
23
|
+
|
|
24
|
+
## Suggesting features
|
|
25
|
+
|
|
26
|
+
Open an issue describing what you want to draw and, ideally, a hand-drawn or
|
|
27
|
+
published example of the intended output. The
|
|
28
|
+
[gallery](https://yohasebe.github.io/rsyntaxtree/examples) shows the range of
|
|
29
|
+
figures the notation covers today; a suggestion that names the linguistic
|
|
30
|
+
framework it serves is easier to evaluate than one described purely in terms
|
|
31
|
+
of graphics.
|
|
32
|
+
|
|
33
|
+
Not every good idea belongs in RSyntaxTree. The project deliberately keeps
|
|
34
|
+
the notation small, so proposals are weighed against what they add for
|
|
35
|
+
users who do not need them.
|
|
36
|
+
|
|
37
|
+
## Setting up for development
|
|
38
|
+
|
|
39
|
+
System libraries first (Pango for text measurement, librsvg for rasterizing):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Debian/Ubuntu
|
|
43
|
+
apt install libpango1.0-dev librsvg2-dev libgirepository1.0-dev gobject-introspection
|
|
44
|
+
|
|
45
|
+
# macOS
|
|
46
|
+
brew install pkg-config pango librsvg gobject-introspection
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The tests measure text with the Noto fonts, so install them too — the
|
|
50
|
+
[Fonts](https://yohasebe.github.io/rsyntaxtree/documentation#fonts) section
|
|
51
|
+
of the manual lists the package names for each platform.
|
|
52
|
+
|
|
53
|
+
Then:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git clone https://github.com/yohasebe/rsyntaxtree.git
|
|
57
|
+
cd rsyntaxtree
|
|
58
|
+
bundle install
|
|
59
|
+
bundle exec rake test
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The tests should pass before and after your change.
|
|
63
|
+
|
|
64
|
+
## Making changes
|
|
65
|
+
|
|
66
|
+
- Every behavioral change needs a test that fails without it.
|
|
67
|
+
- The committed gallery figures are generated inside Docker
|
|
68
|
+
(`rake docker_build && rake docker_generate`) so that font differences
|
|
69
|
+
between machines do not rewrite them. Do not regenerate them with a local
|
|
70
|
+
`rake generate`; if your change intentionally alters figures, say so in
|
|
71
|
+
the pull request and the maintainer will regenerate them.
|
|
72
|
+
- The manual lives in `docs/documentation.md` (English) and
|
|
73
|
+
`docs/documentation_ja.md` (Japanese). If your change affects the
|
|
74
|
+
notation or an option, update at least the English manual; the reference
|
|
75
|
+
the gem installs (`lib/rsyntaxtree/notation_core.md`) and the LLM payload
|
|
76
|
+
(`rake llm_payload`) must be regenerated when examples or the manual
|
|
77
|
+
change — the test suite tells you when they are stale.
|
|
78
|
+
|
|
79
|
+
## Pull requests
|
|
80
|
+
|
|
81
|
+
Fork, branch, and open a pull request against `master`. A good pull request
|
|
82
|
+
explains the problem before the solution and stays small enough to review.
|
|
83
|
+
CI runs the test suite on Linux and an installation check on macOS; both
|
|
84
|
+
must pass.
|
|
85
|
+
|
|
86
|
+
## Questions
|
|
87
|
+
|
|
88
|
+
For anything that does not fit an issue, contact the maintainer:
|
|
89
|
+
Yoichiro Hasebe <yohasebe@gmail.com>.
|
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, TikZ, or
|
|
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
|
|
|
@@ -135,23 +135,17 @@ See [Fonts](https://yohasebe.github.io/rsyntaxtree/documentation#install-fonts-f
|
|
|
135
135
|
gem install rsyntaxtree
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
### macOS
|
|
138
|
+
### macOS
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
gem install rsyntaxtree
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Alternatively, use the [Docker image](https://hub.docker.com/r/yohasebe/rsyntaxtree) or the [web interface](https://yohasebe.com/rsyntaxtree).
|
|
140
|
+
With the libraries in place (`brew install pkg-config pango librsvg
|
|
141
|
+
gobject-introspection`), a plain `gem install rsyntaxtree` builds the native
|
|
142
|
+
extensions without further options — CI verifies this on every change. If an
|
|
143
|
+
older setup still fails to build `gobject-introspection`, `cairo-gobject` or
|
|
144
|
+
`gio2`, install those three first with
|
|
145
|
+
`-- --with-ldflags="-Wl,-undefined,dynamic_lookup"` appended, then install
|
|
146
|
+
RSyntaxTree; or skip the build entirely with the
|
|
147
|
+
[Docker image](https://hub.docker.com/r/yohasebe/rsyntaxtree) or the
|
|
148
|
+
[web interface](https://yohasebe.com/rsyntaxtree).
|
|
155
149
|
|
|
156
150
|
## Usage
|
|
157
151
|
|
|
@@ -170,7 +164,7 @@ Usage:
|
|
|
170
164
|
| Option | Description | Default |
|
|
171
165
|
|--------|-------------|---------|
|
|
172
166
|
| `-o, --outdir` | Output directory | `./` |
|
|
173
|
-
| `-f, --format` | Output format: png, svg, pdf,
|
|
167
|
+
| `-f, --format` | Output format: png, svg, pdf, json, tikz | `png` |
|
|
174
168
|
| `-l, --leafstyle` | Leaf style: auto, triangle, bar, nothing | `auto` |
|
|
175
169
|
| `-n, --fontstyle` | Font style: sans, serif, cjk, mono | `sans` |
|
|
176
170
|
| `-s, --fontsize` | Font size: 6-26 | `16` |
|
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, pdf, svg,
|
|
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,7 +17,7 @@ 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" },
|
|
@@ -133,6 +133,13 @@ opts = Optimist.options do
|
|
|
133
133
|
opt :help, "This is a custom help message", short: :h
|
|
134
134
|
end
|
|
135
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
|
+
|
|
136
143
|
Optimist.die :outdir, "must be an exsting directory path" unless FileTest.directory?(opts[:outdir])
|
|
137
144
|
Optimist.die :direction, "must be one of: #{OPTION_VALUES[:direction].join(', ')}" unless OPTION_VALUES[:direction].include?(opts[:direction])
|
|
138
145
|
Optimist.die :derivation, "must be one of: #{OPTION_VALUES[:derivation].join(', ')}" unless OPTION_VALUES[:derivation].include?(opts[:derivation])
|
|
@@ -207,12 +214,9 @@ data = RSyntaxTree::FormatConverter.to_bracket(data) if data
|
|
|
207
214
|
# Validate without drawing or writing anything: the diagnosis goes to stdout
|
|
208
215
|
# as JSON and the exit code carries the verdict.
|
|
209
216
|
if opts[:validate]
|
|
210
|
-
diagnosis =
|
|
211
|
-
RSyntaxTree::RSGenerator.
|
|
212
|
-
|
|
213
|
-
rescue RSTError => e
|
|
214
|
-
{ "schema" => "rsyntaxtree.error/1" }.merge(e.to_h)
|
|
215
|
-
end
|
|
217
|
+
diagnosis = { "schema" => "rsyntaxtree.error/1" }.merge(
|
|
218
|
+
RSyntaxTree::RSGenerator.diagnose(data, string_opts)
|
|
219
|
+
)
|
|
216
220
|
puts JSON.pretty_generate(diagnosis)
|
|
217
221
|
exit(diagnosis["ok"] ? 0 : 1)
|
|
218
222
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
#==========================
|
|
4
|
-
#
|
|
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
|
|
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
|
|
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
|
|
245
|
-
next if e[:decoration].include?(:whitespace) && e[:text].strip.empty?
|
|
246
|
-
|
|
247
|
-
{
|
|
248
|
-
text: e[:text].gsub(WHITESPACE_BLOCK, " ").gsub('>', '>').gsub('<', '<'),
|
|
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('>', '>').gsub('<', '<'),
|
|
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
|
-
|
|
277
|
-
|
|
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
|
},
|
|
@@ -87,7 +87,7 @@ Options (the command line spells them --like-this):
|
|
|
87
87
|
|
|
88
88
|
| option | what it decides |
|
|
89
89
|
|------------|-----------------|
|
|
90
|
-
| format | png, svg, pdf, tikz,
|
|
90
|
+
| format | png, svg, pdf, tikz, json (the tree as structured data) |
|
|
91
91
|
| fontstyle | sans, serif, mono, cjk |
|
|
92
92
|
| fontsize | 6–26 |
|
|
93
93
|
| color | modern, traditional, off, gray |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
RSyntaxTree examples:
|
|
1
|
+
RSyntaxTree examples: 81 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;
|
|
@@ -2586,30 +2586,56 @@ VAL\t#(SPR\t⟨<>⟩#)#)#)
|
|
|
2586
2586
|
VAL\t#(SPR\t⟨<>*NP*<>⟩#)#)#) [barks]]]
|
|
2587
2587
|
```
|
|
2588
2588
|
|
|
2589
|
-
## 085 — A tree on a tilted plane
|
|
2589
|
+
## 085 — A tree on a tilted plane, with a movement path
|
|
2590
2590
|
|
|
2591
2591
|
Category: General
|
|
2592
2592
|
Settings: fontstyle=noto-serif shear=20 vheight=1.0
|
|
2593
2593
|
|
|
2594
2594
|
```
|
|
2595
|
-
[
|
|
2596
|
-
[NP
|
|
2597
|
-
|
|
2598
|
-
[
|
|
2599
|
-
|
|
2600
|
-
[VP
|
|
2601
|
-
[V put]
|
|
2602
|
-
[NP
|
|
2603
|
-
[D the]
|
|
2604
|
-
[N book]
|
|
2605
|
-
]
|
|
2606
|
-
[PP
|
|
2607
|
-
[P on]
|
|
2595
|
+
[CP
|
|
2596
|
+
[NP what+>1]
|
|
2597
|
+
[C'
|
|
2598
|
+
[C did]
|
|
2599
|
+
[TP
|
|
2608
2600
|
[NP
|
|
2609
2601
|
[D the]
|
|
2610
|
-
[N
|
|
2602
|
+
[N man]
|
|
2603
|
+
]
|
|
2604
|
+
[VP
|
|
2605
|
+
[V'
|
|
2606
|
+
[V put]
|
|
2607
|
+
[NP *t*+1]
|
|
2608
|
+
]
|
|
2609
|
+
[PP
|
|
2610
|
+
[P on]
|
|
2611
|
+
[NP
|
|
2612
|
+
[D the]
|
|
2613
|
+
[N table]
|
|
2614
|
+
]
|
|
2615
|
+
]
|
|
2611
2616
|
]
|
|
2612
2617
|
]
|
|
2613
2618
|
]
|
|
2614
2619
|
]
|
|
2615
2620
|
```
|
|
2621
|
+
|
|
2622
|
+
## 086 — Feature structures on a tilted plane
|
|
2623
|
+
|
|
2624
|
+
Category: General
|
|
2625
|
+
Settings: color=none fontstyle=noto-serif leafstyle=nothing shear=20 vheight=1.5
|
|
2626
|
+
|
|
2627
|
+
```
|
|
2628
|
+
[#(*phrase*\
|
|
2629
|
+
---\
|
|
2630
|
+
SYN\t#(HEAD\t|1|#)#)
|
|
2631
|
+
[#(*word*\
|
|
2632
|
+
---\
|
|
2633
|
+
SYN\t#(HEAD\t#(*noun*\
|
|
2634
|
+
---\
|
|
2635
|
+
AGR\t|2|#(NUM\t*pl*#)#)#)#) [dogs]]
|
|
2636
|
+
[#(*word*\
|
|
2637
|
+
---\
|
|
2638
|
+
SYN\t#(HEAD\t|1|#(*verb*\
|
|
2639
|
+
---\
|
|
2640
|
+
AGR\t|2|#)#)#) [bark]]]
|
|
2641
|
+
```
|
|
@@ -15,9 +15,23 @@ require_relative 'utils'
|
|
|
15
15
|
module RSyntaxTree
|
|
16
16
|
class StringParser
|
|
17
17
|
attr_accessor :data, :elist, :pos, :id, :level
|
|
18
|
+
attr_reader :collected_errors
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
# In collect mode, reporting stops here but the walk does not: labels
|
|
21
|
+
# past this many failures are still checked (the tree must stay whole)
|
|
22
|
+
# without adding to the list.
|
|
23
|
+
COLLECTED_ERRORS_LIMIT = 20
|
|
24
|
+
|
|
25
|
+
# What stands in for a label that would not parse, when collecting.
|
|
26
|
+
# Any label that always parses will do; what matters is that it keeps
|
|
27
|
+
# the tree's shape.
|
|
28
|
+
PLACEHOLDER_LABEL = "x"
|
|
29
|
+
|
|
30
|
+
def initialize(str, fontset, fontsize, global, collect_errors: false)
|
|
20
31
|
@global = global
|
|
32
|
+
@collect_errors = collect_errors
|
|
33
|
+
@collected_errors = []
|
|
34
|
+
@more_errors = false
|
|
21
35
|
# Clean up the data a little to make processing easier
|
|
22
36
|
# repeated newlines => a newline
|
|
23
37
|
string = str.gsub(/[\n\r]+/m, "\n")
|
|
@@ -113,6 +127,41 @@ module RSyntaxTree
|
|
|
113
127
|
restore_rule_names_without_a_rule
|
|
114
128
|
end
|
|
115
129
|
|
|
130
|
+
def more_errors?
|
|
131
|
+
@more_errors
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# One element, or its recorded failure. Outside collect mode this is
|
|
135
|
+
# exactly the Element.new it wraps. In collect mode an RSTError is
|
|
136
|
+
# recorded and a placeholder element stands in, so the walk continues
|
|
137
|
+
# and every bad label is seen. The placeholder keeps the tree's shape —
|
|
138
|
+
# parenthood, childlessness, level — which is all a later label's
|
|
139
|
+
# verdict can depend on; what it cannot keep (a path marker the broken
|
|
140
|
+
# label carried) only matters to the whole-tree checks, and those do
|
|
141
|
+
# not run while collected errors stand.
|
|
142
|
+
def element_or_recorded_failure(id, parent, level, names_a_rule = false)
|
|
143
|
+
yield
|
|
144
|
+
rescue RSTError => e
|
|
145
|
+
raise unless @collect_errors
|
|
146
|
+
|
|
147
|
+
record_failure(e)
|
|
148
|
+
Element.new(id, parent, PLACEHOLDER_LABEL, level, @fontset, @fontsize, @global, names_a_rule)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The same mistake in two copies of a label is one thing to fix, so a
|
|
152
|
+
# failure is recorded once per (code, label, position). Past the limit
|
|
153
|
+
# the list stops growing and only the fact that there was more is kept.
|
|
154
|
+
def record_failure(error)
|
|
155
|
+
key = [error.code, error.label, error.position]
|
|
156
|
+
return if @collected_errors.any? { |c| [c.code, c.label, c.position] == key }
|
|
157
|
+
|
|
158
|
+
if @collected_errors.length >= COLLECTED_ERRORS_LIMIT
|
|
159
|
+
@more_errors = true
|
|
160
|
+
else
|
|
161
|
+
@collected_errors << error
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
116
165
|
# A rule name names the step that produced a node from its daughters. A node
|
|
117
166
|
# with no daughters is the product of no step, so what looked like a name is
|
|
118
167
|
# a column of the label like any other, and it goes back.
|
|
@@ -128,8 +177,17 @@ module RSyntaxTree
|
|
|
128
177
|
next unless e.children.empty?
|
|
129
178
|
next if e.label_with_rule_name.nil?
|
|
130
179
|
|
|
131
|
-
|
|
132
|
-
|
|
180
|
+
# In collect mode the failed restoration is recorded and the element
|
|
181
|
+
# kept as it is; the tree stays whole either way.
|
|
182
|
+
restored = begin
|
|
183
|
+
Element.new(e.id, e.parent, e.label_with_rule_name,
|
|
184
|
+
e.level, @fontset, @fontsize, @global)
|
|
185
|
+
rescue RSTError => err
|
|
186
|
+
raise unless @collect_errors
|
|
187
|
+
|
|
188
|
+
record_failure(err)
|
|
189
|
+
next
|
|
190
|
+
end
|
|
133
191
|
restored.children = e.children
|
|
134
192
|
restored.type = e.type
|
|
135
193
|
@elist.elements[i] = restored
|
|
@@ -226,7 +284,9 @@ module RSyntaxTree
|
|
|
226
284
|
# Check for escaped square brackets
|
|
227
285
|
if token =~ /\A\\\[/ || token =~ /\A\\\]/
|
|
228
286
|
# Treat escaped brackets as regular text
|
|
229
|
-
element =
|
|
287
|
+
element = element_or_recorded_failure(@id, parent, @level) do
|
|
288
|
+
Element.new(@id, parent, token, @level, @fontset, @fontsize, @global)
|
|
289
|
+
end
|
|
230
290
|
@id += 1
|
|
231
291
|
@elist.add(element)
|
|
232
292
|
else
|
|
@@ -241,7 +301,7 @@ module RSyntaxTree
|
|
|
241
301
|
tl = token_r.length
|
|
242
302
|
parts[1] = token_r[spaceat, tl - spaceat].join
|
|
243
303
|
|
|
244
|
-
element =
|
|
304
|
+
element = element_or_recorded_failure(@id, parent, @level, true) do
|
|
245
305
|
Element.new(@id, parent, parts[0], @level, @fontset, @fontsize, @global, true)
|
|
246
306
|
rescue RSTError => e
|
|
247
307
|
# The first raw space splits a token into the node's label
|
|
@@ -277,11 +337,15 @@ module RSyntaxTree
|
|
|
277
337
|
@elist.add(element)
|
|
278
338
|
newparent = element.id
|
|
279
339
|
|
|
280
|
-
element =
|
|
340
|
+
element = element_or_recorded_failure(@id, @id - 1, @level + 1) do
|
|
341
|
+
Element.new(@id, @id - 1, parts[1], @level + 1, @fontset, @fontsize, @global)
|
|
342
|
+
end
|
|
281
343
|
@id += 1
|
|
282
344
|
else
|
|
283
345
|
joined = token_r.join
|
|
284
|
-
element =
|
|
346
|
+
element = element_or_recorded_failure(@id, parent, @level, true) do
|
|
347
|
+
Element.new(@id, parent, joined, @level, @fontset, @fontsize, @global, true)
|
|
348
|
+
end
|
|
285
349
|
@id += 1
|
|
286
350
|
newparent = element.id
|
|
287
351
|
end
|
|
@@ -291,7 +355,9 @@ module RSyntaxTree
|
|
|
291
355
|
end
|
|
292
356
|
else
|
|
293
357
|
if token.strip != ""
|
|
294
|
-
element =
|
|
358
|
+
element = element_or_recorded_failure(@id, parent, @level) do
|
|
359
|
+
Element.new(@id, parent, token, @level, @fontset, @fontsize, @global)
|
|
360
|
+
end
|
|
295
361
|
@id += 1
|
|
296
362
|
@elist.add(element)
|
|
297
363
|
end
|
data/lib/rsyntaxtree/version.rb
CHANGED
data/lib/rsyntaxtree.rb
CHANGED
|
@@ -34,12 +34,12 @@ WHITESPACE_BLOCK = "■"
|
|
|
34
34
|
# Every format the library can produce, and the file extension each one
|
|
35
35
|
# writes. The CLI builds its --format validation, help text, and output
|
|
36
36
|
# filename from these, so a new format is added here and nowhere else.
|
|
37
|
-
FORMATS = %w[png pdf svg
|
|
37
|
+
FORMATS = %w[png pdf svg json tikz].freeze
|
|
38
38
|
FORMAT_EXTENSIONS = {
|
|
39
39
|
"png" => "png",
|
|
40
40
|
"pdf" => "pdf",
|
|
41
41
|
"svg" => "svg",
|
|
42
|
-
"
|
|
42
|
+
"json" => "json",
|
|
43
43
|
"tikz" => "tex" # forest code, to be included in a LaTeX document
|
|
44
44
|
}.freeze
|
|
45
45
|
|
|
@@ -51,7 +51,10 @@ FORMAT_EXTENSIONS = {
|
|
|
51
51
|
# purpose (see switched_on?); unknown keys are not rejected, because callers
|
|
52
52
|
# pass their own extra parameters through.
|
|
53
53
|
OPTION_VALUES = {
|
|
54
|
-
format
|
|
54
|
+
# lsif is the name the JSON format wore from 1.11.0 to 2.0.0, accepted as
|
|
55
|
+
# an alias until 3.0. It collided with a code-intelligence format of the
|
|
56
|
+
# same initials, and claimed an interchange nobody else had implemented.
|
|
57
|
+
format: FORMATS + ["lsif"],
|
|
55
58
|
leafstyle: %w[auto triangle bar nothing],
|
|
56
59
|
fontstyle: ["sans", "serif", "cjk", "mono", "noto-sans", "noto-serif", "noto-sans-mono", "cjk zenhei"],
|
|
57
60
|
color: %w[modern traditional gray grey off none on true false],
|
|
@@ -136,14 +139,20 @@ class RSTError < StandardError
|
|
|
136
139
|
# the top, rather than repeated on every error.
|
|
137
140
|
REFERENCE = "rsyntaxtree --notation, or https://yohasebe.github.io/rsyntaxtree/llms-full.txt"
|
|
138
141
|
|
|
142
|
+
# One error as the hash the JSON diagnosis carries; to_h wraps a single
|
|
143
|
+
# one and diagnose collects many.
|
|
144
|
+
def error_entry
|
|
145
|
+
{ "code" => code.to_s,
|
|
146
|
+
"message" => message,
|
|
147
|
+
"label" => label,
|
|
148
|
+
"position" => position,
|
|
149
|
+
"hint" => hint,
|
|
150
|
+
"retryable" => retryable }.compact
|
|
151
|
+
end
|
|
152
|
+
|
|
139
153
|
def to_h
|
|
140
154
|
{ "ok" => false,
|
|
141
|
-
"errors" => [
|
|
142
|
-
"message" => message,
|
|
143
|
-
"label" => label,
|
|
144
|
-
"position" => position,
|
|
145
|
-
"hint" => hint,
|
|
146
|
-
"retryable" => retryable }.compact],
|
|
155
|
+
"errors" => [error_entry],
|
|
147
156
|
"reference" => REFERENCE }
|
|
148
157
|
end
|
|
149
158
|
end
|
|
@@ -152,7 +161,7 @@ require_relative 'rsyntaxtree/utils'
|
|
|
152
161
|
require_relative 'rsyntaxtree/element'
|
|
153
162
|
require_relative 'rsyntaxtree/elementlist'
|
|
154
163
|
require_relative 'rsyntaxtree/svg_graph'
|
|
155
|
-
require_relative 'rsyntaxtree/
|
|
164
|
+
require_relative 'rsyntaxtree/json_graph'
|
|
156
165
|
require_relative 'rsyntaxtree/tikz_generator'
|
|
157
166
|
require_relative 'rsyntaxtree/version'
|
|
158
167
|
require_relative 'rsyntaxtree/string_parser'
|
|
@@ -182,13 +191,13 @@ module RSyntaxTree
|
|
|
182
191
|
|
|
183
192
|
if REMOVED_OPTIONS.key?(key)
|
|
184
193
|
raise RSTError.new(+"Error: option '#{key}' was removed in RSyntaxTree 2.0",
|
|
185
|
-
code: :invalid_option,
|
|
194
|
+
code: :invalid_option, label: key.to_s,
|
|
186
195
|
hint: "Use #{REMOVED_OPTIONS[key]} instead.",
|
|
187
196
|
retryable: false)
|
|
188
197
|
end
|
|
189
198
|
if OPTION_VALUES.key?(key) && !OPTION_VALUES[key].include?(value.to_s)
|
|
190
199
|
raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
|
|
191
|
-
code: :invalid_option,
|
|
200
|
+
code: :invalid_option, label: key.to_s,
|
|
192
201
|
hint: "'#{key}' must be one of: #{OPTION_VALUES[key].join(', ')}.",
|
|
193
202
|
retryable: false)
|
|
194
203
|
end
|
|
@@ -199,14 +208,14 @@ module RSyntaxTree
|
|
|
199
208
|
if NUMERIC_RANGES.key?(key) && !value.is_a?(Numeric) &&
|
|
200
209
|
value.to_s.strip !~ /\A-?(\d+(\.\d+)?|\.\d+)\z/
|
|
201
210
|
raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
|
|
202
|
-
code: :invalid_option,
|
|
211
|
+
code: :invalid_option, label: key.to_s,
|
|
203
212
|
hint: "'#{key}' takes a number.",
|
|
204
213
|
retryable: false)
|
|
205
214
|
end
|
|
206
215
|
if NUMERIC_RANGES.key?(key) && !NUMERIC_RANGES[key].cover?(value.to_f)
|
|
207
216
|
range = NUMERIC_RANGES[key]
|
|
208
217
|
raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
|
|
209
|
-
code: :invalid_option,
|
|
218
|
+
code: :invalid_option, label: key.to_s,
|
|
210
219
|
hint: "'#{key}' must be in the range of #{range.begin}-#{range.end}.",
|
|
211
220
|
retryable: false)
|
|
212
221
|
end
|
|
@@ -263,6 +272,8 @@ module RSyntaxTree
|
|
|
263
272
|
else
|
|
264
273
|
"off"
|
|
265
274
|
end
|
|
275
|
+
when :format
|
|
276
|
+
new_params[key] = value.to_s == "lsif" ? "json" : value.to_s
|
|
266
277
|
when :hyphen
|
|
267
278
|
new_params[key] = value.to_s == "literal" ? "literal" : "markup"
|
|
268
279
|
when :fontsize
|
|
@@ -283,7 +294,7 @@ module RSyntaxTree
|
|
|
283
294
|
else
|
|
284
295
|
unless COLOR_NAMES.include?(v.downcase) || v =~ /\A#(\h{3}|\h{6})\z/
|
|
285
296
|
raise RSTError.new(+"Error: invalid value for option 'shear_plane': #{value.inspect}",
|
|
286
|
-
code: :invalid_option,
|
|
297
|
+
code: :invalid_option, label: "shear_plane",
|
|
287
298
|
hint: "'shear_plane' is on, off, a colour name, " \
|
|
288
299
|
"or a hex colour of 3 or 6 digits.",
|
|
289
300
|
retryable: false)
|
|
@@ -328,7 +339,7 @@ module RSyntaxTree
|
|
|
328
339
|
# the combination is refused rather than approximated.
|
|
329
340
|
if @params[:derivation] == true && @params[:direction] == "ltr"
|
|
330
341
|
raise RSTError.new(+"Error: a derivation cannot be drawn left to right",
|
|
331
|
-
code: :invalid_option,
|
|
342
|
+
code: :invalid_option, label: "derivation",
|
|
332
343
|
hint: "A derivation runs down the page. Use direction ttb or btt, " \
|
|
333
344
|
"or turn derivation off.",
|
|
334
345
|
retryable: false)
|
|
@@ -341,7 +352,7 @@ module RSyntaxTree
|
|
|
341
352
|
# them. Refused for the same reason as left to right.
|
|
342
353
|
if @params[:derivation] == true && @params[:hide_default_connectors] == true
|
|
343
354
|
raise RSTError.new(+"Error: a derivation's rules cannot be hidden",
|
|
344
|
-
code: :invalid_option,
|
|
355
|
+
code: :invalid_option, label: "derivation",
|
|
345
356
|
hint: "The rules are what a derivation is drawn with, not a " \
|
|
346
357
|
"connector added to it. Turn off hide default connectors, " \
|
|
347
358
|
"or turn derivation off.",
|
|
@@ -443,6 +454,101 @@ module RSyntaxTree
|
|
|
443
454
|
end
|
|
444
455
|
end
|
|
445
456
|
|
|
457
|
+
# Every option error at once. The constructor is the only judge of an
|
|
458
|
+
# option, and its rules are not written out a second time here: it
|
|
459
|
+
# stops at its first complaint, so it is asked again with the option
|
|
460
|
+
# it complained about set aside, until it accepts what is left or
|
|
461
|
+
# names nothing to set aside. Each round removes one option, so the
|
|
462
|
+
# loop is as bounded as the option list.
|
|
463
|
+
def self.option_errors(params)
|
|
464
|
+
remaining = params.reject { |k, _| k.to_sym == :data }
|
|
465
|
+
errors = []
|
|
466
|
+
loop do
|
|
467
|
+
begin
|
|
468
|
+
new(remaining.merge(data: "[A a]"))
|
|
469
|
+
break
|
|
470
|
+
rescue RSTError => e
|
|
471
|
+
errors << e
|
|
472
|
+
key = e.label
|
|
473
|
+
break if key.nil? || remaining.keys.none? { |k| k.to_s == key }
|
|
474
|
+
|
|
475
|
+
remaining = remaining.reject { |k, _| k.to_s == key }
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
errors
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Parse in collect mode: every label that will not parse, and every
|
|
482
|
+
# rule name that turns out to have no rule behind it, recorded instead
|
|
483
|
+
# of raised. The walk is the real one — the same tokens, the same
|
|
484
|
+
# judgments — so what this reports and what drawing rejects cannot
|
|
485
|
+
# drift apart. Returns the errors and whether the list was cut short.
|
|
486
|
+
def collect_input_errors
|
|
487
|
+
sp = StringParser.new(@params[:data].gsub('&', '&'), @params[:fontset],
|
|
488
|
+
@params[:fontsize], @global, collect_errors: true)
|
|
489
|
+
sp.parse
|
|
490
|
+
[sp.collected_errors, sp.more_errors?]
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
NOTE_OPTIONS = "Not every option could be read, so the input itself has " \
|
|
494
|
+
"not been checked yet; fixing the options may reveal more."
|
|
495
|
+
NOTE_STRUCTURE = "The bracket structure could not be read, so the labels " \
|
|
496
|
+
"have not been checked yet; fixing it may reveal more."
|
|
497
|
+
NOTE_LABELS = "Whole-tree checks (paths, output limits) run only once " \
|
|
498
|
+
"every label reads, so fixing these may reveal more."
|
|
499
|
+
NOTE_TRUNCATED = " Only the first #{StringParser::COLLECTED_ERRORS_LIMIT} " \
|
|
500
|
+
"problems are listed."
|
|
501
|
+
|
|
502
|
+
# The whole diagnosis at once, as the hash the CLI prints: every error
|
|
503
|
+
# of the first stage that finds any, not just the first error found.
|
|
504
|
+
# Stages are ordered so that nothing is reported whose appearance is an
|
|
505
|
+
# artifact of an earlier mistake — a missing bracket shifts every token
|
|
506
|
+
# after it, an unreadable hyphen option changes what counts as markup —
|
|
507
|
+
# and when a stage stops the walk, the note says that fixing what is
|
|
508
|
+
# listed may reveal more. check_data keeps its contract (raise the
|
|
509
|
+
# first error) for callers that want a verdict rather than a list.
|
|
510
|
+
def self.diagnose(text, params = {})
|
|
511
|
+
errors = []
|
|
512
|
+
note = nil
|
|
513
|
+
if text.to_s == ""
|
|
514
|
+
errors << RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false)
|
|
515
|
+
else
|
|
516
|
+
begin
|
|
517
|
+
errors = option_errors(params)
|
|
518
|
+
if errors.any?
|
|
519
|
+
note = NOTE_OPTIONS
|
|
520
|
+
else
|
|
521
|
+
StringParser.valid?(text)
|
|
522
|
+
gen = new(params.merge(data: text))
|
|
523
|
+
collected, truncated = gen.collect_input_errors
|
|
524
|
+
if collected.any?
|
|
525
|
+
errors = collected
|
|
526
|
+
note = NOTE_LABELS
|
|
527
|
+
note += NOTE_TRUNCATED if truncated
|
|
528
|
+
else
|
|
529
|
+
gen.validate!
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
rescue RSTError => e
|
|
533
|
+
errors << e
|
|
534
|
+
note = NOTE_STRUCTURE if %i[empty_brackets unbalanced_brackets].include?(e.code)
|
|
535
|
+
rescue StandardError => e
|
|
536
|
+
# The same promise check_data makes: a defect anywhere in here —
|
|
537
|
+
# the option probing included — is still a verdict of "no", in
|
|
538
|
+
# the same shape, not a backtrace.
|
|
539
|
+
errors << RSTError.new(+"Error: input could not be processed (#{e.class})",
|
|
540
|
+
code: :internal_error, retryable: false)
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
return { "ok" => true } if errors.empty?
|
|
545
|
+
|
|
546
|
+
result = { "ok" => false, "errors" => errors.map(&:error_entry) }
|
|
547
|
+
result["note"] = note if note
|
|
548
|
+
result["reference"] = RSTError::REFERENCE
|
|
549
|
+
result
|
|
550
|
+
end
|
|
551
|
+
|
|
446
552
|
# Generate, and throw the result away. Parsing alone leaves out the
|
|
447
553
|
# checks that only happen once the tree is laid out — a movement path
|
|
448
554
|
# with one end, a line with three — so validation that stopped at the
|
|
@@ -461,7 +567,7 @@ module RSyntaxTree
|
|
|
461
567
|
case @params[:format]
|
|
462
568
|
when "png" then raster_surface_for(draw_svg, &:finish)
|
|
463
569
|
when "pdf" then pdf_surface_for(draw_svg, StringIO.new, &:finish)
|
|
464
|
-
when "
|
|
570
|
+
when "json" then draw_json
|
|
465
571
|
when "tikz" then draw_tikz
|
|
466
572
|
else draw_svg
|
|
467
573
|
end
|
|
@@ -535,11 +641,16 @@ module RSyntaxTree
|
|
|
535
641
|
graph.svg_data
|
|
536
642
|
end
|
|
537
643
|
|
|
538
|
-
def
|
|
644
|
+
def draw_json
|
|
539
645
|
sp = StringParser.new(@params[:data].gsub('&', '&'), @params[:fontset], @params[:fontsize], @global)
|
|
540
646
|
sp.parse
|
|
541
|
-
graph =
|
|
542
|
-
graph.
|
|
647
|
+
graph = JSONGraph.new(sp.get_elementlist, @params, @global)
|
|
648
|
+
graph.json_data
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# The name the JSON output wore from 1.11.0 to 2.0.0; removed in 3.0.
|
|
652
|
+
def draw_lsif
|
|
653
|
+
draw_json
|
|
543
654
|
end
|
|
544
655
|
|
|
545
656
|
def draw_tikz(standalone: false, font: nil)
|
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.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yoichiro Hasebe
|
|
@@ -115,8 +115,11 @@ executables:
|
|
|
115
115
|
extensions: []
|
|
116
116
|
extra_rdoc_files: []
|
|
117
117
|
files:
|
|
118
|
+
- ".dockerignore"
|
|
118
119
|
- CHANGELOG.md
|
|
119
120
|
- CITATION.cff
|
|
121
|
+
- CODE_OF_CONDUCT.md
|
|
122
|
+
- CONTRIBUTING.md
|
|
120
123
|
- LICENSE
|
|
121
124
|
- README.md
|
|
122
125
|
- bin/rsyntaxtree
|
|
@@ -126,7 +129,7 @@ files:
|
|
|
126
129
|
- lib/rsyntaxtree/element.rb
|
|
127
130
|
- lib/rsyntaxtree/elementlist.rb
|
|
128
131
|
- lib/rsyntaxtree/format_converter.rb
|
|
129
|
-
- lib/rsyntaxtree/
|
|
132
|
+
- lib/rsyntaxtree/json_graph.rb
|
|
130
133
|
- lib/rsyntaxtree/markup_parser.rb
|
|
131
134
|
- lib/rsyntaxtree/notation_core.md
|
|
132
135
|
- lib/rsyntaxtree/notation_examples.md
|