rsyntaxtree 2.1.0 → 2.3.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: 231de78b2e34e37f4a5bbccd641839e8cf5ae1babc2d8d4362d86dcf782d28b4
4
- data.tar.gz: a4d04e71e6587592540400aa9a7e23d9dadae0d19912078d5d6cc5ebd65c117d
3
+ metadata.gz: 8c57f743f27cc6830495cfe9da3ce4483fec13491af8e80d4437357cc9c37968
4
+ data.tar.gz: d051b212a5abe64711fd42622bce06cea77c1e8bfc78a8315dfc1246f0dc22d0
5
5
  SHA512:
6
- metadata.gz: f97797690b6d8e0ad8404c00a10c061c89d2213e582915a33fc9d739305d5fb10cdd0afde6bffe0b16f504b5d5854d7dcb0249f50a22b10599638fb22180aed3
7
- data.tar.gz: 7f5814cc9bf0875b21982cf6c143397d467fb900a7973c8f6213934387ba4f80e0605ddccec63f02bc7bacc6d182a9da1bda3237f909414ba4c64f63a1da1d12
6
+ metadata.gz: e214718246e31cf0d42a03f91ee4b7e714cf1576f3f60fb70b6d0fb9d2d22a24e2b04e832d9e8be2df0b664b5506bf8a0994f659384a85469a4023535409c615
7
+ data.tar.gz: 5f7638450414b9cb53ac9fe2cdc08e7776af24ea50a0de75166dfbb3d47b9d4a27ab1d45a026012ef6bd9fdcc76400b1cf88f7df5095dac20cd78c629c3ccc33
data/.dockerignore ADDED
@@ -0,0 +1,5 @@
1
+ # The image needs the working tree, not the repository: without this, ADD .
2
+ # copies .git into every image — hundreds of megabytes of history, including
3
+ # refs that exist only on the build machine.
4
+ .git
5
+ pkg
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.0] - 2026-09-01
4
+
5
+ ### Added
6
+ - `RSTError::CODES` lists every error code the library can report. A program
7
+ keyed by code — one that translates the diagnosis, or tallies what kinds of
8
+ mistake a writer makes — needs the whole set rather than the codes it has
9
+ happened to see, and until now that set could only be recovered by reading
10
+ five files and the repair table. The list only grows: a published code is
11
+ not renamed or removed.
12
+
13
+ ### Fixed
14
+ - Whitespace alone is an empty input rather than a defect in the library. It
15
+ reached the drawing, which walked off the end of an empty tree and reported
16
+ `internal_error`, telling the caller that their own fixable input was the
17
+ library's fault.
18
+ - A bracketless input of a single character draws. The tokenizer counted its
19
+ cursor as exhausted one character early, so `A`, and `<>` once it is one
20
+ whitespace block, produced no tokens at all — though the manual has always
21
+ said a label on its own draws as one leaf.
22
+
23
+ ## [2.2.0] - 2026-09-01
24
+
25
+ ### Changed
26
+ - Validation reports every error of a stage, not just the first one found.
27
+ An input with three bad labels lists all three in the `errors` array of
28
+ the JSON diagnosis, so one round of fixes covers them. The stages are
29
+ ordered — options, bracket structure, labels, whole-tree checks — and a
30
+ mistake stops the later stages, with a `note` saying that fixing what is
31
+ listed may reveal more; nothing is reported whose appearance is an
32
+ artifact of an earlier mistake. Duplicate failures collapse to one entry
33
+ and the list stops at twenty. The schema is unchanged
34
+ (`rsyntaxtree.error/1`): the same array, more of it.
35
+
36
+ ### Added
37
+ - `RSGenerator.diagnose(text, options)` returns the full diagnosis as a
38
+ hash — what `--validate` prints — for programmatic callers.
39
+ `check_data` keeps its contract of raising the first error.
40
+
3
41
  ## [2.1.0] - 2026-08-29
4
42
 
5
43
  ### Changed
@@ -79,9 +117,8 @@ looking at what was left. Every figure in the gallery draws exactly as it did.
79
117
 
80
118
  ### Added
81
119
  - `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
120
+ leaning the top to the right) and lies on a plane drawn behind it, which
121
+ shows the tilt as a surface. The whole picture shears as one affine piece layout untouched, region
85
122
  shades coming out parallelograms on their own, and nothing able to newly
86
123
  touch or cross. The plane is fill without an edge — a region shade is
87
124
  bounded because it marks one part of a figure off from the rest, and a
@@ -227,9 +264,8 @@ looking at what was left. Every figure in the gallery draws exactly as it did.
227
264
  - Derivations. A derivation puts the words first and the result last, and
228
265
  joins what each step combines with one rule drawn across all of it rather
229
266
  than with a line to each daughter. `derivation` draws the rules and
230
- `direction: btt` turns the tree over; together they give the format
231
- categorial grammar is written in, and `derivation` on its own marks the
232
- spans of an ordinary tree. The name of each step rides in the label after a
267
+ `direction: btt` turns the tree over; together they draw a CCG derivation,
268
+ and `derivation` on its own marks the spans of an ordinary tree. The name of each step rides in the label after a
233
269
  column break and is set beside the end of its rule. A derivation runs down
234
270
  the page and is drawn with its rules, so `direction: ltr` and hiding the
235
271
  default connectors are refused rather than approximated.
@@ -361,15 +397,15 @@ RMagick dependency.
361
397
  rule that work never uses.
362
398
  - A matrix nested in a label, written between `#(` and `#)`. The value of an
363
399
  attribute can be another attribute-value matrix, to any depth, which is what
364
- a feature path such as SYNSEM | LOCAL | CATEGORY | HEAD needs and what HPSG,
365
- SBCG and LFG are written in. The nested matrix draws its own brackets and
400
+ a feature path such as SYNSEM | LOCAL | CATEGORY | HEAD needs, and what
401
+ HPSG and LFG are written in. The nested matrix draws its own brackets and
366
402
  lays out its own columns, and the rows after it clear its full height.
367
403
  - `\t` in a label cuts the line into cells. Every line is cut at the same
368
404
  points and each column is drawn at the width of its widest cell, so the
369
405
  parts line up down the label. Together with the bracket enclosure and the
370
406
  horizontal rule this gives attribute-value matrices — the feature structures
371
- of HPSG, SBCG and LFG — without spacing each row by hand, which is how the
372
- gallery's HPSG example used to do it.
407
+ of HPSG, LFG and the like — without spacing each row by hand, which is how
408
+ the gallery's HPSG example used to do it.
373
409
  - `color: gray`, a scheme that keeps node and leaf labels black and draws the
374
410
  connectors, triangles and movement paths in grey. It is for diagrams whose
375
411
  links outnumber their labels — an ontology, a network of constructions —
@@ -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
@@ -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 Installation Notice
139
-
140
- **Important for macOS users:** If you encounter build errors for native extensions (`gobject-introspection`, `cairo-gobject`, `gio2`), run the following commands before installing RSyntaxTree:
141
-
142
- ```bash
143
- gem install gobject-introspection -- --with-ldflags="-Wl,-undefined,dynamic_lookup"
144
- gem install cairo-gobject -- --with-ldflags="-Wl,-undefined,dynamic_lookup"
145
- gem install gio2 -- --with-ldflags="-Wl,-undefined,dynamic_lookup"
146
- ```
147
-
148
- Then install RSyntaxTree:
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).
138
+ ### macOS
139
+
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
 
data/bin/rsyntaxtree CHANGED
@@ -214,12 +214,9 @@ data = RSyntaxTree::FormatConverter.to_bracket(data) if data
214
214
  # Validate without drawing or writing anything: the diagnosis goes to stdout
215
215
  # as JSON and the exit code carries the verdict.
216
216
  if opts[:validate]
217
- diagnosis = begin
218
- RSyntaxTree::RSGenerator.check_data(data, string_opts)
219
- { "schema" => "rsyntaxtree.error/1", "ok" => true }
220
- rescue RSTError => e
221
- { "schema" => "rsyntaxtree.error/1" }.merge(e.to_h)
222
- end
217
+ diagnosis = { "schema" => "rsyntaxtree.error/1" }.merge(
218
+ RSyntaxTree::RSGenerator.diagnose(data, string_opts)
219
+ )
223
220
  puts JSON.pretty_generate(diagnosis)
224
221
  exit(diagnosis["ok"] ? 0 : 1)
225
222
  end
@@ -1,4 +1,4 @@
1
- RSyntaxTree examples: 80 trees, every one verified to draw.
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
- [S
2596
- [NP
2597
- [D the]
2598
- [N man]
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 table]
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
- def initialize(str, fontset, fontsize, global)
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")
@@ -50,7 +64,12 @@ module RSyntaxTree
50
64
  end
51
65
 
52
66
  def self.valid?(data)
53
- raise RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false) if data.empty?
67
+ # Whitespace alone is an empty input, not a tree with one blank label:
68
+ # nothing survives the parse, and what used to happen is that the
69
+ # drawing walked off the end of an empty element list and reported a
70
+ # defect in the library for what the caller could see and fix. A space
71
+ # meant as a label is written <>, which is not whitespace here.
72
+ raise RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false) if data.strip.empty?
54
73
 
55
74
  if /\[\s*\]/m =~ data
56
75
  raise RSTError.new(+"Error: inside the brackets is empty", code: :empty_brackets,
@@ -113,6 +132,41 @@ module RSyntaxTree
113
132
  restore_rule_names_without_a_rule
114
133
  end
115
134
 
135
+ def more_errors?
136
+ @more_errors
137
+ end
138
+
139
+ # One element, or its recorded failure. Outside collect mode this is
140
+ # exactly the Element.new it wraps. In collect mode an RSTError is
141
+ # recorded and a placeholder element stands in, so the walk continues
142
+ # and every bad label is seen. The placeholder keeps the tree's shape —
143
+ # parenthood, childlessness, level — which is all a later label's
144
+ # verdict can depend on; what it cannot keep (a path marker the broken
145
+ # label carried) only matters to the whole-tree checks, and those do
146
+ # not run while collected errors stand.
147
+ def element_or_recorded_failure(id, parent, level, names_a_rule = false)
148
+ yield
149
+ rescue RSTError => e
150
+ raise unless @collect_errors
151
+
152
+ record_failure(e)
153
+ Element.new(id, parent, PLACEHOLDER_LABEL, level, @fontset, @fontsize, @global, names_a_rule)
154
+ end
155
+
156
+ # The same mistake in two copies of a label is one thing to fix, so a
157
+ # failure is recorded once per (code, label, position). Past the limit
158
+ # the list stops growing and only the fact that there was more is kept.
159
+ def record_failure(error)
160
+ key = [error.code, error.label, error.position]
161
+ return if @collected_errors.any? { |c| [c.code, c.label, c.position] == key }
162
+
163
+ if @collected_errors.length >= COLLECTED_ERRORS_LIMIT
164
+ @more_errors = true
165
+ else
166
+ @collected_errors << error
167
+ end
168
+ end
169
+
116
170
  # A rule name names the step that produced a node from its daughters. A node
117
171
  # with no daughters is the product of no step, so what looked like a name is
118
172
  # a column of the label like any other, and it goes back.
@@ -128,8 +182,17 @@ module RSyntaxTree
128
182
  next unless e.children.empty?
129
183
  next if e.label_with_rule_name.nil?
130
184
 
131
- restored = Element.new(e.id, e.parent, e.label_with_rule_name,
132
- e.level, @fontset, @fontsize, @global)
185
+ # In collect mode the failed restoration is recorded and the element
186
+ # kept as it is; the tree stays whole either way.
187
+ restored = begin
188
+ Element.new(e.id, e.parent, e.label_with_rule_name,
189
+ e.level, @fontset, @fontsize, @global)
190
+ rescue RSTError => err
191
+ raise unless @collect_errors
192
+
193
+ record_failure(err)
194
+ next
195
+ end
133
196
  restored.children = e.children
134
197
  restored.type = e.type
135
198
  @elist.elements[i] = restored
@@ -146,7 +209,11 @@ module RSyntaxTree
146
209
  token = ""
147
210
  i = 0
148
211
 
149
- return "" if (@pos + 1) >= data.length
212
+ # The cursor is exhausted when it reaches the end, not one before it:
213
+ # counting from @pos + 1 skipped the last character, so an input of a
214
+ # single character — "A", or "<>" once it is one whitespace block —
215
+ # produced no tokens at all and drew from an empty element list.
216
+ return "" if @pos >= data.length
150
217
 
151
218
  escape = false
152
219
  while ((@pos + i) < data.length) && !gottoken
@@ -226,7 +293,9 @@ module RSyntaxTree
226
293
  # Check for escaped square brackets
227
294
  if token =~ /\A\\\[/ || token =~ /\A\\\]/
228
295
  # Treat escaped brackets as regular text
229
- element = Element.new(@id, parent, token, @level, @fontset, @fontsize, @global)
296
+ element = element_or_recorded_failure(@id, parent, @level) do
297
+ Element.new(@id, parent, token, @level, @fontset, @fontsize, @global)
298
+ end
230
299
  @id += 1
231
300
  @elist.add(element)
232
301
  else
@@ -241,7 +310,7 @@ module RSyntaxTree
241
310
  tl = token_r.length
242
311
  parts[1] = token_r[spaceat, tl - spaceat].join
243
312
 
244
- element = begin
313
+ element = element_or_recorded_failure(@id, parent, @level, true) do
245
314
  Element.new(@id, parent, parts[0], @level, @fontset, @fontsize, @global, true)
246
315
  rescue RSTError => e
247
316
  # The first raw space splits a token into the node's label
@@ -277,11 +346,15 @@ module RSyntaxTree
277
346
  @elist.add(element)
278
347
  newparent = element.id
279
348
 
280
- element = Element.new(@id, @id - 1, parts[1], @level + 1, @fontset, @fontsize, @global)
349
+ element = element_or_recorded_failure(@id, @id - 1, @level + 1) do
350
+ Element.new(@id, @id - 1, parts[1], @level + 1, @fontset, @fontsize, @global)
351
+ end
281
352
  @id += 1
282
353
  else
283
354
  joined = token_r.join
284
- element = Element.new(@id, parent, joined, @level, @fontset, @fontsize, @global, true)
355
+ element = element_or_recorded_failure(@id, parent, @level, true) do
356
+ Element.new(@id, parent, joined, @level, @fontset, @fontsize, @global, true)
357
+ end
285
358
  @id += 1
286
359
  newparent = element.id
287
360
  end
@@ -291,7 +364,9 @@ module RSyntaxTree
291
364
  end
292
365
  else
293
366
  if token.strip != ""
294
- element = Element.new(@id, parent, token, @level, @fontset, @fontsize, @global)
367
+ element = element_or_recorded_failure(@id, parent, @level) do
368
+ Element.new(@id, parent, token, @level, @fontset, @fontsize, @global)
369
+ end
295
370
  @id += 1
296
371
  @elist.add(element)
297
372
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSyntaxTree
4
- VERSION = "2.1.0"
4
+ VERSION = "2.3.0"
5
5
  end
data/lib/rsyntaxtree.rb CHANGED
@@ -133,20 +133,70 @@ class RSTError < StandardError
133
133
  super(msg.gsub(WHITESPACE_BLOCK, "<>"))
134
134
  end
135
135
 
136
+ # Every code an error from this library can carry. Consumers dispatch on
137
+ # these — a translation keyed by code, a harness counting what kinds of
138
+ # mistake occur — and both need to know the whole set, not only the codes
139
+ # they have happened to see: a kind of mistake nobody made is not the same
140
+ # as a kind that cannot happen. Until this list existed the set was only
141
+ # discoverable by reading five files and a repair table.
142
+ #
143
+ # Written out rather than computed, because it is a contract and a contract
144
+ # should be readable at a glance and visible in a diff. A test keeps it
145
+ # honest by finding the codes the library actually raises and comparing.
146
+ #
147
+ # The list grows and does not churn: a code once published is not renamed
148
+ # or removed, so a consumer keyed by code — a translation table, a tally of
149
+ # what kinds of mistake a writer makes — only ever has entries to add.
150
+ #
151
+ # Eight are named by the repair table, which diagnoses a label by rewriting
152
+ # it and asking the parser whether the rewrite reads: angle_brackets,
153
+ # bare_hyphen, incomplete_path, invalid_color, rule_name_without_derivation,
154
+ # stray_triangle, unclosed_markup, unclosed_matrix. invalid_markup is where
155
+ # a label lands whose mistake no single rewrite fits, which in practice
156
+ # means more than one. unknown_color and label_split are mistakes inside a
157
+ # label too, found before the repair table is reached.
158
+ CODES = %i[
159
+ angle_brackets
160
+ bare_hyphen
161
+ empty_brackets
162
+ empty_input
163
+ incomplete_path
164
+ internal_error
165
+ invalid_color
166
+ invalid_markup
167
+ invalid_option
168
+ label_split
169
+ path_multiple_ends
170
+ path_single_end
171
+ result_too_big
172
+ rule_name_without_derivation
173
+ stray_triangle
174
+ unbalanced_brackets
175
+ unclosed_markup
176
+ unclosed_matrix
177
+ unknown_color
178
+ ].freeze
179
+
136
180
  # Where the notation is written down. A hint repairs the mistake in front of
137
181
  # it and says nothing about the rest, which is enough for a reader who knows
138
182
  # the notation and not enough for one who was guessing at it. Given once, at
139
183
  # the top, rather than repeated on every error.
140
184
  REFERENCE = "rsyntaxtree --notation, or https://yohasebe.github.io/rsyntaxtree/llms-full.txt"
141
185
 
186
+ # One error as the hash the JSON diagnosis carries; to_h wraps a single
187
+ # one and diagnose collects many.
188
+ def error_entry
189
+ { "code" => code.to_s,
190
+ "message" => message,
191
+ "label" => label,
192
+ "position" => position,
193
+ "hint" => hint,
194
+ "retryable" => retryable }.compact
195
+ end
196
+
142
197
  def to_h
143
198
  { "ok" => false,
144
- "errors" => [{ "code" => code.to_s,
145
- "message" => message,
146
- "label" => label,
147
- "position" => position,
148
- "hint" => hint,
149
- "retryable" => retryable }.compact],
199
+ "errors" => [error_entry],
150
200
  "reference" => REFERENCE }
151
201
  end
152
202
  end
@@ -185,13 +235,13 @@ module RSyntaxTree
185
235
 
186
236
  if REMOVED_OPTIONS.key?(key)
187
237
  raise RSTError.new(+"Error: option '#{key}' was removed in RSyntaxTree 2.0",
188
- code: :invalid_option,
238
+ code: :invalid_option, label: key.to_s,
189
239
  hint: "Use #{REMOVED_OPTIONS[key]} instead.",
190
240
  retryable: false)
191
241
  end
192
242
  if OPTION_VALUES.key?(key) && !OPTION_VALUES[key].include?(value.to_s)
193
243
  raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
194
- code: :invalid_option,
244
+ code: :invalid_option, label: key.to_s,
195
245
  hint: "'#{key}' must be one of: #{OPTION_VALUES[key].join(', ')}.",
196
246
  retryable: false)
197
247
  end
@@ -202,14 +252,14 @@ module RSyntaxTree
202
252
  if NUMERIC_RANGES.key?(key) && !value.is_a?(Numeric) &&
203
253
  value.to_s.strip !~ /\A-?(\d+(\.\d+)?|\.\d+)\z/
204
254
  raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
205
- code: :invalid_option,
255
+ code: :invalid_option, label: key.to_s,
206
256
  hint: "'#{key}' takes a number.",
207
257
  retryable: false)
208
258
  end
209
259
  if NUMERIC_RANGES.key?(key) && !NUMERIC_RANGES[key].cover?(value.to_f)
210
260
  range = NUMERIC_RANGES[key]
211
261
  raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
212
- code: :invalid_option,
262
+ code: :invalid_option, label: key.to_s,
213
263
  hint: "'#{key}' must be in the range of #{range.begin}-#{range.end}.",
214
264
  retryable: false)
215
265
  end
@@ -288,7 +338,7 @@ module RSyntaxTree
288
338
  else
289
339
  unless COLOR_NAMES.include?(v.downcase) || v =~ /\A#(\h{3}|\h{6})\z/
290
340
  raise RSTError.new(+"Error: invalid value for option 'shear_plane': #{value.inspect}",
291
- code: :invalid_option,
341
+ code: :invalid_option, label: "shear_plane",
292
342
  hint: "'shear_plane' is on, off, a colour name, " \
293
343
  "or a hex colour of 3 or 6 digits.",
294
344
  retryable: false)
@@ -333,7 +383,7 @@ module RSyntaxTree
333
383
  # the combination is refused rather than approximated.
334
384
  if @params[:derivation] == true && @params[:direction] == "ltr"
335
385
  raise RSTError.new(+"Error: a derivation cannot be drawn left to right",
336
- code: :invalid_option,
386
+ code: :invalid_option, label: "derivation",
337
387
  hint: "A derivation runs down the page. Use direction ttb or btt, " \
338
388
  "or turn derivation off.",
339
389
  retryable: false)
@@ -346,7 +396,7 @@ module RSyntaxTree
346
396
  # them. Refused for the same reason as left to right.
347
397
  if @params[:derivation] == true && @params[:hide_default_connectors] == true
348
398
  raise RSTError.new(+"Error: a derivation's rules cannot be hidden",
349
- code: :invalid_option,
399
+ code: :invalid_option, label: "derivation",
350
400
  hint: "The rules are what a derivation is drawn with, not a " \
351
401
  "connector added to it. Turn off hide default connectors, " \
352
402
  "or turn derivation off.",
@@ -429,8 +479,21 @@ module RSyntaxTree
429
479
  !OFF.include?(value.to_s.strip.downcase)
430
480
  end
431
481
 
482
+ # Whether the input is nothing to draw. Whitespace alone counts: it
483
+ # leaves no label behind, and reporting it as a defect in the library
484
+ # told the caller their own fixable input was our fault. Asked without
485
+ # strip when the bytes are not valid UTF-8, because strip raises there
486
+ # and that input is not blank — it is malformed, which the caller
487
+ # learns further down.
488
+ def self.blank?(text)
489
+ s = text.to_s
490
+ return true if s.empty?
491
+
492
+ s.valid_encoding? && s.strip.empty?
493
+ end
494
+
432
495
  def self.check_data(text, params = {})
433
- raise RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false) if text.to_s == ""
496
+ raise RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false) if blank?(text)
434
497
 
435
498
  begin
436
499
  StringParser.valid?(text)
@@ -448,6 +511,101 @@ module RSyntaxTree
448
511
  end
449
512
  end
450
513
 
514
+ # Every option error at once. The constructor is the only judge of an
515
+ # option, and its rules are not written out a second time here: it
516
+ # stops at its first complaint, so it is asked again with the option
517
+ # it complained about set aside, until it accepts what is left or
518
+ # names nothing to set aside. Each round removes one option, so the
519
+ # loop is as bounded as the option list.
520
+ def self.option_errors(params)
521
+ remaining = params.reject { |k, _| k.to_sym == :data }
522
+ errors = []
523
+ loop do
524
+ begin
525
+ new(remaining.merge(data: "[A a]"))
526
+ break
527
+ rescue RSTError => e
528
+ errors << e
529
+ key = e.label
530
+ break if key.nil? || remaining.keys.none? { |k| k.to_s == key }
531
+
532
+ remaining = remaining.reject { |k, _| k.to_s == key }
533
+ end
534
+ end
535
+ errors
536
+ end
537
+
538
+ # Parse in collect mode: every label that will not parse, and every
539
+ # rule name that turns out to have no rule behind it, recorded instead
540
+ # of raised. The walk is the real one — the same tokens, the same
541
+ # judgments — so what this reports and what drawing rejects cannot
542
+ # drift apart. Returns the errors and whether the list was cut short.
543
+ def collect_input_errors
544
+ sp = StringParser.new(@params[:data].gsub('&', '&amp;'), @params[:fontset],
545
+ @params[:fontsize], @global, collect_errors: true)
546
+ sp.parse
547
+ [sp.collected_errors, sp.more_errors?]
548
+ end
549
+
550
+ NOTE_OPTIONS = "Not every option could be read, so the input itself has " \
551
+ "not been checked yet; fixing the options may reveal more."
552
+ NOTE_STRUCTURE = "The bracket structure could not be read, so the labels " \
553
+ "have not been checked yet; fixing it may reveal more."
554
+ NOTE_LABELS = "Whole-tree checks (paths, output limits) run only once " \
555
+ "every label reads, so fixing these may reveal more."
556
+ NOTE_TRUNCATED = " Only the first #{StringParser::COLLECTED_ERRORS_LIMIT} " \
557
+ "problems are listed."
558
+
559
+ # The whole diagnosis at once, as the hash the CLI prints: every error
560
+ # of the first stage that finds any, not just the first error found.
561
+ # Stages are ordered so that nothing is reported whose appearance is an
562
+ # artifact of an earlier mistake — a missing bracket shifts every token
563
+ # after it, an unreadable hyphen option changes what counts as markup —
564
+ # and when a stage stops the walk, the note says that fixing what is
565
+ # listed may reveal more. check_data keeps its contract (raise the
566
+ # first error) for callers that want a verdict rather than a list.
567
+ def self.diagnose(text, params = {})
568
+ errors = []
569
+ note = nil
570
+ if blank?(text)
571
+ errors << RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false)
572
+ else
573
+ begin
574
+ errors = option_errors(params)
575
+ if errors.any?
576
+ note = NOTE_OPTIONS
577
+ else
578
+ StringParser.valid?(text)
579
+ gen = new(params.merge(data: text))
580
+ collected, truncated = gen.collect_input_errors
581
+ if collected.any?
582
+ errors = collected
583
+ note = NOTE_LABELS
584
+ note += NOTE_TRUNCATED if truncated
585
+ else
586
+ gen.validate!
587
+ end
588
+ end
589
+ rescue RSTError => e
590
+ errors << e
591
+ note = NOTE_STRUCTURE if %i[empty_brackets unbalanced_brackets].include?(e.code)
592
+ rescue StandardError => e
593
+ # The same promise check_data makes: a defect anywhere in here —
594
+ # the option probing included — is still a verdict of "no", in
595
+ # the same shape, not a backtrace.
596
+ errors << RSTError.new(+"Error: input could not be processed (#{e.class})",
597
+ code: :internal_error, retryable: false)
598
+ end
599
+ end
600
+
601
+ return { "ok" => true } if errors.empty?
602
+
603
+ result = { "ok" => false, "errors" => errors.map(&:error_entry) }
604
+ result["note"] = note if note
605
+ result["reference"] = RSTError::REFERENCE
606
+ result
607
+ end
608
+
451
609
  # Generate, and throw the result away. Parsing alone leaves out the
452
610
  # checks that only happen once the tree is laid out — a movement path
453
611
  # with one end, a line with three — so validation that stopped at the
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.1.0
4
+ version: 2.3.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