rsyntaxtree 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 231de78b2e34e37f4a5bbccd641839e8cf5ae1babc2d8d4362d86dcf782d28b4
4
- data.tar.gz: a4d04e71e6587592540400aa9a7e23d9dadae0d19912078d5d6cc5ebd65c117d
3
+ metadata.gz: fe4cd7a8b28ffa968f99ff228e3a182a64d420e0405cd27e120b5f4ff02fe48e
4
+ data.tar.gz: 23a64934f448707de2b55cad4c206d65fab18aa7d6c1cd3aab0b1871efd9be1e
5
5
  SHA512:
6
- metadata.gz: f97797690b6d8e0ad8404c00a10c061c89d2213e582915a33fc9d739305d5fb10cdd0afde6bffe0b16f504b5d5854d7dcb0249f50a22b10599638fb22180aed3
7
- data.tar.gz: 7f5814cc9bf0875b21982cf6c143397d467fb900a7973c8f6213934387ba4f80e0605ddccec63f02bc7bacc6d182a9da1bda3237f909414ba4c64f63a1da1d12
6
+ metadata.gz: 2dfd53e10a62187649fb3d7285fa38189ef25cf56d6af7ceea27fba3cc7358b1f7d528db6466607061120fab61a2ff7bd9b84fa0b989e4313faaac306af42235
7
+ data.tar.gz: 7399787ea179bdd17910fa9695cc292a18cb5b8e9b3948e943bf32fd52fd05f89e6ec0e2792138197d56c839b9960bab678489a976590667eca9b4dd80679029
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,23 @@
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
+
3
21
  ## [2.1.0] - 2026-08-29
4
22
 
5
23
  ### Changed
@@ -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")
@@ -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
- restored = Element.new(e.id, e.parent, e.label_with_rule_name,
132
- e.level, @fontset, @fontsize, @global)
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 = Element.new(@id, parent, token, @level, @fontset, @fontsize, @global)
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 = begin
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 = Element.new(@id, @id - 1, parts[1], @level + 1, @fontset, @fontsize, @global)
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 = Element.new(@id, parent, joined, @level, @fontset, @fontsize, @global, true)
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 = Element.new(@id, parent, token, @level, @fontset, @fontsize, @global)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSyntaxTree
4
- VERSION = "2.1.0"
4
+ VERSION = "2.2.0"
5
5
  end
data/lib/rsyntaxtree.rb CHANGED
@@ -139,14 +139,20 @@ class RSTError < StandardError
139
139
  # the top, rather than repeated on every error.
140
140
  REFERENCE = "rsyntaxtree --notation, or https://yohasebe.github.io/rsyntaxtree/llms-full.txt"
141
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
+
142
153
  def to_h
143
154
  { "ok" => false,
144
- "errors" => [{ "code" => code.to_s,
145
- "message" => message,
146
- "label" => label,
147
- "position" => position,
148
- "hint" => hint,
149
- "retryable" => retryable }.compact],
155
+ "errors" => [error_entry],
150
156
  "reference" => REFERENCE }
151
157
  end
152
158
  end
@@ -185,13 +191,13 @@ module RSyntaxTree
185
191
 
186
192
  if REMOVED_OPTIONS.key?(key)
187
193
  raise RSTError.new(+"Error: option '#{key}' was removed in RSyntaxTree 2.0",
188
- code: :invalid_option,
194
+ code: :invalid_option, label: key.to_s,
189
195
  hint: "Use #{REMOVED_OPTIONS[key]} instead.",
190
196
  retryable: false)
191
197
  end
192
198
  if OPTION_VALUES.key?(key) && !OPTION_VALUES[key].include?(value.to_s)
193
199
  raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
194
- code: :invalid_option,
200
+ code: :invalid_option, label: key.to_s,
195
201
  hint: "'#{key}' must be one of: #{OPTION_VALUES[key].join(', ')}.",
196
202
  retryable: false)
197
203
  end
@@ -202,14 +208,14 @@ module RSyntaxTree
202
208
  if NUMERIC_RANGES.key?(key) && !value.is_a?(Numeric) &&
203
209
  value.to_s.strip !~ /\A-?(\d+(\.\d+)?|\.\d+)\z/
204
210
  raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
205
- code: :invalid_option,
211
+ code: :invalid_option, label: key.to_s,
206
212
  hint: "'#{key}' takes a number.",
207
213
  retryable: false)
208
214
  end
209
215
  if NUMERIC_RANGES.key?(key) && !NUMERIC_RANGES[key].cover?(value.to_f)
210
216
  range = NUMERIC_RANGES[key]
211
217
  raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
212
- code: :invalid_option,
218
+ code: :invalid_option, label: key.to_s,
213
219
  hint: "'#{key}' must be in the range of #{range.begin}-#{range.end}.",
214
220
  retryable: false)
215
221
  end
@@ -288,7 +294,7 @@ module RSyntaxTree
288
294
  else
289
295
  unless COLOR_NAMES.include?(v.downcase) || v =~ /\A#(\h{3}|\h{6})\z/
290
296
  raise RSTError.new(+"Error: invalid value for option 'shear_plane': #{value.inspect}",
291
- code: :invalid_option,
297
+ code: :invalid_option, label: "shear_plane",
292
298
  hint: "'shear_plane' is on, off, a colour name, " \
293
299
  "or a hex colour of 3 or 6 digits.",
294
300
  retryable: false)
@@ -333,7 +339,7 @@ module RSyntaxTree
333
339
  # the combination is refused rather than approximated.
334
340
  if @params[:derivation] == true && @params[:direction] == "ltr"
335
341
  raise RSTError.new(+"Error: a derivation cannot be drawn left to right",
336
- code: :invalid_option,
342
+ code: :invalid_option, label: "derivation",
337
343
  hint: "A derivation runs down the page. Use direction ttb or btt, " \
338
344
  "or turn derivation off.",
339
345
  retryable: false)
@@ -346,7 +352,7 @@ module RSyntaxTree
346
352
  # them. Refused for the same reason as left to right.
347
353
  if @params[:derivation] == true && @params[:hide_default_connectors] == true
348
354
  raise RSTError.new(+"Error: a derivation's rules cannot be hidden",
349
- code: :invalid_option,
355
+ code: :invalid_option, label: "derivation",
350
356
  hint: "The rules are what a derivation is drawn with, not a " \
351
357
  "connector added to it. Turn off hide default connectors, " \
352
358
  "or turn derivation off.",
@@ -448,6 +454,101 @@ module RSyntaxTree
448
454
  end
449
455
  end
450
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('&', '&amp;'), @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
+
451
552
  # Generate, and throw the result away. Parsing alone leaves out the
452
553
  # checks that only happen once the tree is laid out — a movement path
453
554
  # 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.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