rsyntaxtree 2.2.0 → 2.4.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/CHANGELOG.md +53 -10
- data/lib/rsyntaxtree/element.rb +31 -10
- data/lib/rsyntaxtree/escape.rb +105 -0
- data/lib/rsyntaxtree/markup_parser.rb +12 -3
- data/lib/rsyntaxtree/notation_core.md +8 -1
- data/lib/rsyntaxtree/string_parser.rb +12 -3
- data/lib/rsyntaxtree/svg_graph.rb +5 -1
- data/lib/rsyntaxtree/version.rb +1 -1
- data/lib/rsyntaxtree.rb +60 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2cab83f32f9b3adab3a84ef896406e8683cf3a9abcecfe132540bda198475a10
|
|
4
|
+
data.tar.gz: 29d0804c7d76c1661103e9dcbc922ea7b916ba503f3fb0c9a8f2d78c8e8008d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22ddead5114613c3a1c82ce9774911e4c414721495b66ed152f904f55ba8aef3d8dde957a473dc563c764635f2892f58f7f2d0d74dd6142427e3de279e34d5c4
|
|
7
|
+
data.tar.gz: 4be4281b2e805ac45c0c01a0cc4d67b837fac67118c807812e88038ecf08fef63952897ba8fab3c66d2ec2b5a66d84428de4e038d1409ee766ab371c17fa907c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.0] - 2026-09-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `RSyntaxTree.escape(text, as:, hyphen:, apostrophe:)` returns notation that
|
|
7
|
+
draws as the given text, for a program writing notation from strings it did
|
|
8
|
+
not choose. The context (`:word`, `:phrase`, `:label`, `:cell`) decides what
|
|
9
|
+
whitespace means; the hyphen mode has to be given because no one spelling of
|
|
10
|
+
a hyphen serves both; `apostrophe: :keep` keeps a straight apostrophe. The
|
|
11
|
+
test draws every result and reads the text back, so the rules are exercised
|
|
12
|
+
rather than restated. The one text with no spelling under `hyphen: literal`
|
|
13
|
+
— a line of nothing but hyphens, which is the rule — raises `ArgumentError`.
|
|
14
|
+
- `\'` keeps a straight apostrophe, which the notation otherwise sets as a
|
|
15
|
+
curly one. `\@` is an at sign.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `\+` before digits is a plus sign. The path markers at the end of a label
|
|
19
|
+
(`+1`, `+>2`) were detected without regard to a backslash, so no label could
|
|
20
|
+
contain "+1" at all: every "C++11" and "2+2" was read as one end of a
|
|
21
|
+
movement path.
|
|
22
|
+
- A label containing a backslash could lose characters in the drawing. The
|
|
23
|
+
text was placed into the SVG as a replacement string, which Ruby reads for
|
|
24
|
+
backreferences: `\+` and `\'` were two of them.
|
|
25
|
+
- An escaped character the tokenizer did not know (`\'`, `\@`) lost its
|
|
26
|
+
backslash before the markup was read.
|
|
27
|
+
|
|
28
|
+
## [2.3.0] - 2026-09-01
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- `RSTError::CODES` lists every error code the library can report. A program
|
|
32
|
+
keyed by code — one that translates the diagnosis, or tallies what kinds of
|
|
33
|
+
mistake a writer makes — needs the whole set rather than the codes it has
|
|
34
|
+
happened to see, and until now that set could only be recovered by reading
|
|
35
|
+
five files and the repair table. The list only grows: a published code is
|
|
36
|
+
not renamed or removed.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Whitespace alone is an empty input rather than a defect in the library. It
|
|
40
|
+
reached the drawing, which walked off the end of an empty tree and reported
|
|
41
|
+
`internal_error`, telling the caller that their own fixable input was the
|
|
42
|
+
library's fault.
|
|
43
|
+
- A bracketless input of a single character draws. The tokenizer counted its
|
|
44
|
+
cursor as exhausted one character early, so `A`, and `<>` once it is one
|
|
45
|
+
whitespace block, produced no tokens at all — though the manual has always
|
|
46
|
+
said a label on its own draws as one leaf.
|
|
47
|
+
|
|
3
48
|
## [2.2.0] - 2026-09-01
|
|
4
49
|
|
|
5
50
|
### Changed
|
|
@@ -97,9 +142,8 @@ looking at what was left. Every figure in the gallery draws exactly as it did.
|
|
|
97
142
|
|
|
98
143
|
### Added
|
|
99
144
|
- `shear`: the drawn figure tilts by the given angle (degrees, positive
|
|
100
|
-
leaning the top to the right) and lies on a plane drawn behind it,
|
|
101
|
-
|
|
102
|
-
whole picture shears as one affine piece — layout untouched, region
|
|
145
|
+
leaning the top to the right) and lies on a plane drawn behind it, which
|
|
146
|
+
shows the tilt as a surface. The whole picture shears as one affine piece — layout untouched, region
|
|
103
147
|
shades coming out parallelograms on their own, and nothing able to newly
|
|
104
148
|
touch or cross. The plane is fill without an edge — a region shade is
|
|
105
149
|
bounded because it marks one part of a figure off from the rest, and a
|
|
@@ -245,9 +289,8 @@ looking at what was left. Every figure in the gallery draws exactly as it did.
|
|
|
245
289
|
- Derivations. A derivation puts the words first and the result last, and
|
|
246
290
|
joins what each step combines with one rule drawn across all of it rather
|
|
247
291
|
than with a line to each daughter. `derivation` draws the rules and
|
|
248
|
-
`direction: btt` turns the tree over; together they
|
|
249
|
-
|
|
250
|
-
spans of an ordinary tree. The name of each step rides in the label after a
|
|
292
|
+
`direction: btt` turns the tree over; together they draw a CCG derivation,
|
|
293
|
+
and `derivation` on its own marks the spans of an ordinary tree. The name of each step rides in the label after a
|
|
251
294
|
column break and is set beside the end of its rule. A derivation runs down
|
|
252
295
|
the page and is drawn with its rules, so `direction: ltr` and hiding the
|
|
253
296
|
default connectors are refused rather than approximated.
|
|
@@ -379,15 +422,15 @@ RMagick dependency.
|
|
|
379
422
|
rule that work never uses.
|
|
380
423
|
- A matrix nested in a label, written between `#(` and `#)`. The value of an
|
|
381
424
|
attribute can be another attribute-value matrix, to any depth, which is what
|
|
382
|
-
a feature path such as SYNSEM | LOCAL | CATEGORY | HEAD needs and what
|
|
383
|
-
|
|
425
|
+
a feature path such as SYNSEM | LOCAL | CATEGORY | HEAD needs, and what
|
|
426
|
+
HPSG and LFG are written in. The nested matrix draws its own brackets and
|
|
384
427
|
lays out its own columns, and the rows after it clear its full height.
|
|
385
428
|
- `\t` in a label cuts the line into cells. Every line is cut at the same
|
|
386
429
|
points and each column is drawn at the width of its widest cell, so the
|
|
387
430
|
parts line up down the label. Together with the bracket enclosure and the
|
|
388
431
|
horizontal rule this gives attribute-value matrices — the feature structures
|
|
389
|
-
of HPSG,
|
|
390
|
-
gallery's HPSG example used to do it.
|
|
432
|
+
of HPSG, LFG and the like — without spacing each row by hand, which is how
|
|
433
|
+
the gallery's HPSG example used to do it.
|
|
391
434
|
- `color: gray`, a scheme that keeps node and leaf labels black and draws the
|
|
392
435
|
connectors, triangles and movement paths in grey. It is for diagrams whose
|
|
393
436
|
links outnumber their labels — an ontology, a network of constructions —
|
data/lib/rsyntaxtree/element.rb
CHANGED
|
@@ -33,11 +33,8 @@ module RSyntaxTree
|
|
|
33
33
|
@vertical_indent = 0 # Drawing offset
|
|
34
34
|
content = content.strip
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
else
|
|
39
|
-
[]
|
|
40
|
-
end
|
|
36
|
+
_body, path_text = Element.split_path(content)
|
|
37
|
+
@path = path_text.delete_prefix("^").delete_suffix("^").sub(/\A\+/, "").split("+")
|
|
41
38
|
|
|
42
39
|
@fontset = fontset
|
|
43
40
|
@fontsize = fontsize
|
|
@@ -64,7 +61,7 @@ module RSyntaxTree
|
|
|
64
61
|
end
|
|
65
62
|
end
|
|
66
63
|
|
|
67
|
-
@raw_content =
|
|
64
|
+
@raw_content = Element.split_path(content).first
|
|
68
65
|
|
|
69
66
|
parsed = Markup.parse(prepare_markup(content))
|
|
70
67
|
|
|
@@ -125,9 +122,33 @@ module RSyntaxTree
|
|
|
125
122
|
# something else: a `---` rule line of its own, and the `+-2` markers
|
|
126
123
|
# at the end. Shared by swap_hyphen_markup and escape_hyphens.
|
|
127
124
|
def self.hyphen_safe_lines(text)
|
|
128
|
-
path = text
|
|
129
|
-
body
|
|
130
|
-
|
|
125
|
+
body, path = split_path(text)
|
|
126
|
+
[body.split('\n', -1), path]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The run of path markers a label ends in: `+1`, `+>2`, `+-3`, several of
|
|
130
|
+
# them, an optional `^` on either side. A marker's `+` is a marker only
|
|
131
|
+
# when the backslashes before it are even in number: `x\+1` is a plus
|
|
132
|
+
# sign and a digit, `x\\+1` a backslash and a marker. Detection used to
|
|
133
|
+
# ignore the backslash, so a label could not contain "+1" at all — every
|
|
134
|
+
# "C++11" and "2+2" was read as one end of a movement path.
|
|
135
|
+
PATH_MARKERS = /(?:\+-?>?<?\d+)+/
|
|
136
|
+
|
|
137
|
+
# Splits a label into what is drawn and its trailing path text, the
|
|
138
|
+
# latter empty when there is none. A label that is nothing but markers
|
|
139
|
+
# has no path: there would be nothing to draw them from.
|
|
140
|
+
def self.split_path(text)
|
|
141
|
+
i = 0
|
|
142
|
+
while (i = text.index("+", i))
|
|
143
|
+
tail = text[i..]
|
|
144
|
+
if i.positive? && tail.match?(/\A#{PATH_MARKERS}\^?\z/) &&
|
|
145
|
+
text[0...i][/\\*\z/].length.even?
|
|
146
|
+
i -= 1 if i > 1 && text[i - 1] == "^"
|
|
147
|
+
return [text[0...i], text[i..]]
|
|
148
|
+
end
|
|
149
|
+
i += 1
|
|
150
|
+
end
|
|
151
|
+
[text, ""]
|
|
131
152
|
end
|
|
132
153
|
|
|
133
154
|
def swap_hyphen_markup(text)
|
|
@@ -366,7 +387,7 @@ module RSyntaxTree
|
|
|
366
387
|
# as a curly apostrophe (U+2019) for smarter typography, e.g. the
|
|
367
388
|
# X-bar prime in "T'". Applied before metrics so the measured glyph
|
|
368
389
|
# matches the rendered one.
|
|
369
|
-
text = text.gsub("'", "’")
|
|
390
|
+
text = text.gsub("'", "’").gsub(Markup::KEPT_APOSTROPHE, "'")
|
|
370
391
|
e[:text] = text.gsub(" ", WHITESPACE_BLOCK)
|
|
371
392
|
.gsub(">", '>')
|
|
372
393
|
.gsub("<", '<')
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#==========================
|
|
4
|
+
# escape.rb
|
|
5
|
+
#==========================
|
|
6
|
+
#
|
|
7
|
+
# Turns arbitrary text into notation that draws as that text.
|
|
8
|
+
# Copyright (c) 2007-2026 Yoichiro Hasebe <yohasebe@gmail.com>
|
|
9
|
+
|
|
10
|
+
module RSyntaxTree
|
|
11
|
+
# The characters the notation reads as markup, each written as itself by
|
|
12
|
+
# a backslash. The backslash comes first so that the others are escaped
|
|
13
|
+
# once, not twice. A yen sign is here because the reader treats it as a
|
|
14
|
+
# backslash for keyboards that have no other.
|
|
15
|
+
ESCAPED_CHARACTERS = %w[\\ [ ] < > ^ + * _ = ~ | # { } % @ ¥].freeze
|
|
16
|
+
|
|
17
|
+
ESCAPE_CONTEXTS = %i[word phrase label cell].freeze
|
|
18
|
+
|
|
19
|
+
# The web interface's transport spellings of characters a form cannot
|
|
20
|
+
# carry, read out of the input before anything else. Under hyphen: markup
|
|
21
|
+
# the hyphens are escaped and nothing matches; under :literal they would
|
|
22
|
+
# be read, so a backslash goes in after the first hyphen — an escape the
|
|
23
|
+
# reader does not know, whose backslash it drops — and the text survives.
|
|
24
|
+
TRANSPORT_PLACEHOLDERS = %w[AMP PERCENT PRIME SCOLON OABRACKET CABRACKET].freeze
|
|
25
|
+
|
|
26
|
+
# A line of three or more hyphens and nothing else is the horizontal rule.
|
|
27
|
+
RULE_LINE = /\A-{3,}\z/
|
|
28
|
+
|
|
29
|
+
# Notation that draws as +text+, for a program writing notation from
|
|
30
|
+
# strings it did not choose — a tagger's tokens, a corpus's words.
|
|
31
|
+
# Written here rather than in every such program, because a copy of the
|
|
32
|
+
# escaping rules kept elsewhere would drift from the notation as it
|
|
33
|
+
# changes, and the drift would show as a different figure drawn without
|
|
34
|
+
# complaint. The test for this method draws each result and checks that
|
|
35
|
+
# the text came out, so the rules are never restated, only exercised.
|
|
36
|
+
#
|
|
37
|
+
# +as+ says where the text will stand, which decides what whitespace
|
|
38
|
+
# means: a :word is one leaf or one label (a space inside it becomes <>),
|
|
39
|
+
# a :phrase is a leaf of several words (spaces stay, so the leaf gets a
|
|
40
|
+
# triangle), a :label is a node label (like a word, with a newline kept
|
|
41
|
+
# as a line break), and a :cell is one cell of a column-aligned or
|
|
42
|
+
# matrix label (tabs and newlines become the column and row breaks).
|
|
43
|
+
#
|
|
44
|
+
# +hyphen+ must match the option the figure is drawn with. Under :markup
|
|
45
|
+
# a hyphen is escaped; under :literal a bare hyphen is already itself,
|
|
46
|
+
# and the escaped form means an underline instead — so there is no
|
|
47
|
+
# notation that reads as a hyphen under both, and the caller has to say.
|
|
48
|
+
#
|
|
49
|
+
# +apostrophe+ :curly leaves a straight apostrophe to be set as a curly
|
|
50
|
+
# one, which is what the notation does; :keep escapes it so it stays
|
|
51
|
+
# straight, for text that must come out exactly as given.
|
|
52
|
+
#
|
|
53
|
+
# One text has no spelling under hyphen: :literal — a line of nothing but
|
|
54
|
+
# three or more hyphens, which is the horizontal rule and draws as one —
|
|
55
|
+
# and asking for it raises ArgumentError rather than returning notation
|
|
56
|
+
# that draws an empty leaf. Empty and all-whitespace text is returned as
|
|
57
|
+
# the whitespace it holds, which draws as a blank; a caller building a
|
|
58
|
+
# tree from tokens will usually want to drop such tokens first.
|
|
59
|
+
def self.escape(text, as: :word, hyphen: :markup, apostrophe: :curly)
|
|
60
|
+
unless ESCAPE_CONTEXTS.include?(as)
|
|
61
|
+
raise ArgumentError, "as: must be one of #{ESCAPE_CONTEXTS.join(', ')}"
|
|
62
|
+
end
|
|
63
|
+
raise ArgumentError, "hyphen: must be :markup or :literal" unless %i[markup literal].include?(hyphen)
|
|
64
|
+
raise ArgumentError, "apostrophe: must be :curly or :keep" unless %i[curly keep].include?(apostrophe)
|
|
65
|
+
|
|
66
|
+
s = text.to_s
|
|
67
|
+
if as == :phrase
|
|
68
|
+
words = s.split(/\s+/).reject(&:empty?)
|
|
69
|
+
# One word alone is the whole leaf; with company, hyphens are text.
|
|
70
|
+
refuse_rule_line(s, hyphen) if hyphen == :literal && words.length == 1 && words.first.match?(RULE_LINE)
|
|
71
|
+
return words.map { |w| escape_text(w, :word, hyphen, apostrophe) }.join(" ")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if hyphen == :literal
|
|
75
|
+
# A newline is a line break in a label or a cell, and a space in a word.
|
|
76
|
+
lines = %i[label cell].include?(as) ? s.split(/\r\n?|\n/, -1) : [s]
|
|
77
|
+
lines.each { |line| refuse_rule_line(s, hyphen) if line.match?(RULE_LINE) }
|
|
78
|
+
end
|
|
79
|
+
escape_text(s, as, hyphen, apostrophe)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The character work, once the text has been accepted.
|
|
83
|
+
def self.escape_text(s, as, hyphen, apostrophe)
|
|
84
|
+
# Every replacement is a block: as a replacement string a backslash is
|
|
85
|
+
# read for backreferences, and `\\'` and `\\+` are two of them.
|
|
86
|
+
out = s.gsub(/[#{Regexp.escape(ESCAPED_CHARACTERS.join)}]/) { |c| "\\#{c}" }
|
|
87
|
+
out = out.gsub("-") { "\\-" } if hyphen == :markup
|
|
88
|
+
if hyphen == :literal
|
|
89
|
+
out = out.gsub(/-(#{TRANSPORT_PLACEHOLDERS.join('|')})-/) { "-\\#{Regexp.last_match(1)}-" }
|
|
90
|
+
end
|
|
91
|
+
out = out.gsub("'") { "\\'" } if apostrophe == :keep
|
|
92
|
+
out = out.gsub(/\r\n?|\n/) { "\\n" } if %i[label cell].include?(as)
|
|
93
|
+
out = out.gsub("\t") { "\\t" } if as == :cell
|
|
94
|
+
# A CRLF is one break, so one space, as it is one break in a label.
|
|
95
|
+
out.gsub(/\r\n|\s/) { "<>" }
|
|
96
|
+
end
|
|
97
|
+
private_class_method :escape_text
|
|
98
|
+
|
|
99
|
+
def self.refuse_rule_line(text, hyphen)
|
|
100
|
+
raise ArgumentError,
|
|
101
|
+
"no notation draws #{text.inspect} under hyphen: #{hyphen.inspect}: a line of " \
|
|
102
|
+
"nothing but hyphens is the horizontal rule. Draw with hyphen: :markup."
|
|
103
|
+
end
|
|
104
|
+
private_class_method :refuse_rule_line
|
|
105
|
+
end
|
|
@@ -39,9 +39,13 @@ class MarkupParser < Parslet::Parser
|
|
|
39
39
|
|
|
40
40
|
rule(:path) { (str('+') >> str('-').maybe >> (str('>') | str('<')).maybe >> match('\d').repeat(1)).as(:path) }
|
|
41
41
|
# rule(:escaped) { str('\\') >> match('[#<>{}\\^+*_=~\|\n\-]').as(:chr) }
|
|
42
|
-
rule(:escaped) { str('\\') >> match('[#<>{}\\\\^+*_=~\\|\\n\\-\\[\\]
|
|
42
|
+
rule(:escaped) { str('\\') >> match('[#<>{}\\\\^+*_=~\\|\\n\\-\\[\\]%@]').as(:chr) }
|
|
43
|
+
# A straight apostrophe is set as a curly one; `\'` asks for the straight
|
|
44
|
+
# one itself. It comes through as a placeholder so the substitution, which
|
|
45
|
+
# runs later on the joined text, can tell the two apart.
|
|
46
|
+
rule(:kept_apostrophe) { str("\\'").as(:kept_apostrophe) }
|
|
43
47
|
rule(:non_escaped) { ((match('[#<>{}\\^+*_=~\|\-]') | str('\\n') | str('\\t')).absent? >> any).as(:chr) }
|
|
44
|
-
rule(:text) { (escaped | non_escaped).repeat(1).as(:text) }
|
|
48
|
+
rule(:text) { (kept_apostrophe | escaped | non_escaped).repeat(1).as(:text) }
|
|
45
49
|
|
|
46
50
|
# Column separator. Every line of a label is cut at these into cells, and
|
|
47
51
|
# each column is laid out at the width of its widest cell, which is what an
|
|
@@ -87,7 +91,7 @@ class MarkupParser < Parslet::Parser
|
|
|
87
91
|
rule(:matrix_line) { (rule_line | double_rule_line | matrix_markup.repeat(1).as(:line)) >> (cr | str('#)').present?) }
|
|
88
92
|
rule(:matrix_markup) { (matrix | tabstop | matrix_text | decoration | shape | bstroke) }
|
|
89
93
|
# Text inside a matrix stops at the closing delimiter as well.
|
|
90
|
-
rule(:matrix_text) { (escaped | (str('#)').absent? >> non_escaped)).repeat(1).as(:text) }
|
|
94
|
+
rule(:matrix_text) { (kept_apostrophe | escaped | (str('#)').absent? >> non_escaped)).repeat(1).as(:text) }
|
|
91
95
|
|
|
92
96
|
rule(:markup) { (matrix | tabstop | text | decoration | shape | bstroke) }
|
|
93
97
|
|
|
@@ -107,10 +111,15 @@ class MarkupParser < Parslet::Parser
|
|
|
107
111
|
end
|
|
108
112
|
|
|
109
113
|
module Markup
|
|
114
|
+
# Stands in for an escaped apostrophe until the curly substitution has
|
|
115
|
+
# run. A private-use character no label carries.
|
|
116
|
+
KEPT_APOSTROPHE = "\uE000"
|
|
117
|
+
|
|
110
118
|
@parser = MarkupParser.new
|
|
111
119
|
|
|
112
120
|
@evaluator = Parslet::Transform.new do
|
|
113
121
|
rule(chr: simple(:chr)) { chr.to_s }
|
|
122
|
+
rule(kept_apostrophe: simple(:kept)) { KEPT_APOSTROPHE }
|
|
114
123
|
rule(text: sequence(:text)) { { text: text.join(""), decoration: [] } }
|
|
115
124
|
|
|
116
125
|
rule(tabstop: subtree(:empty)) {
|
|
@@ -24,9 +24,16 @@ figure that was meant.
|
|
|
24
24
|
`\n` or `\t` in it: there it can split a value that carries markup, and it
|
|
25
25
|
breaks a nested #( ... #) matrix. Inside any label with columns, write a
|
|
26
26
|
space as `<>`: 'a<>toy', not 'a toy'.
|
|
27
|
-
- A straight apostrophe is typeset as the curly ’ (U+2019)
|
|
27
|
+
- A straight apostrophe is typeset as the curly ’ (U+2019); \' keeps it
|
|
28
|
+
straight.
|
|
29
|
+
- A `+` followed by digits at the end of a label is a movement path (+1, +>2).
|
|
30
|
+
\+ is a plus sign, so C\+\+11 and 2\+2 draw as written.
|
|
28
31
|
- A backslash takes the character after it, whatever it is: \q draws q, and
|
|
29
32
|
C:\path draws C:path. A backslash itself is \\.
|
|
33
|
+
- A program building labels from text it did not choose should not copy
|
|
34
|
+
these rules: RSyntaxTree.escape(text, as: :word) returns notation that
|
|
35
|
+
draws as the text (as: :phrase, :label or :cell for the other places;
|
|
36
|
+
hyphen: to match the hyphen option; apostrophe: :keep to keep it straight).
|
|
30
37
|
- The prefixes of a label compose in one order, and only this one:
|
|
31
38
|
`^` → `#`/`##`/`###` → `%` → `@color:`. So ^#%@red:NP is read whole, while
|
|
32
39
|
@red:%NP leaves the % as a literal character and silently drops the shade.
|
|
@@ -64,7 +64,12 @@ module RSyntaxTree
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def self.valid?(data)
|
|
67
|
-
|
|
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?
|
|
68
73
|
|
|
69
74
|
if /\[\s*\]/m =~ data
|
|
70
75
|
raise RSTError.new(+"Error: inside the brackets is empty", code: :empty_brackets,
|
|
@@ -204,7 +209,11 @@ module RSyntaxTree
|
|
|
204
209
|
token = ""
|
|
205
210
|
i = 0
|
|
206
211
|
|
|
207
|
-
|
|
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
|
|
208
217
|
|
|
209
218
|
escape = false
|
|
210
219
|
while ((@pos + i) < data.length) && !gottoken
|
|
@@ -247,7 +256,7 @@ module RSyntaxTree
|
|
|
247
256
|
# `\#` arrived at the grammar as a bare '#', which opens an enclosure,
|
|
248
257
|
# so a label written with a hash in it lost the hash and everything
|
|
249
258
|
# after it went inside brackets instead.
|
|
250
|
-
when /[nt{}<>^+*_
|
|
259
|
+
when /[nt{}<>^+*_=~|%\-#@']/
|
|
251
260
|
if escape
|
|
252
261
|
token += '\\' + ch
|
|
253
262
|
escape = false
|
|
@@ -623,7 +623,11 @@ module RSyntaxTree
|
|
|
623
623
|
element.ink_top = first_baseline - f_m.ink_above
|
|
624
624
|
element.ink_bottom = text_y + (l_m.ink_height - l_m.ink_above)
|
|
625
625
|
end
|
|
626
|
-
|
|
626
|
+
# The block form, so the label goes in as it is. As a replacement
|
|
627
|
+
# string it was read for backreferences, and `\+` — the last matched
|
|
628
|
+
# group, of which there is none — took a backslash and a plus sign out
|
|
629
|
+
# of any label that carried them.
|
|
630
|
+
@tree_data += text_data.sub(/CONTENT/) { new_text }
|
|
627
631
|
end
|
|
628
632
|
|
|
629
633
|
# Draws one run of a label — a stretch of text with its decorations, or the
|
data/lib/rsyntaxtree/version.rb
CHANGED
data/lib/rsyntaxtree.rb
CHANGED
|
@@ -133,6 +133,50 @@ 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
|
|
@@ -166,6 +210,7 @@ require_relative 'rsyntaxtree/tikz_generator'
|
|
|
166
210
|
require_relative 'rsyntaxtree/version'
|
|
167
211
|
require_relative 'rsyntaxtree/string_parser'
|
|
168
212
|
require_relative 'rsyntaxtree/format_converter'
|
|
213
|
+
require_relative 'rsyntaxtree/escape'
|
|
169
214
|
|
|
170
215
|
require 'cgi'
|
|
171
216
|
require 'rsvg2'
|
|
@@ -435,8 +480,21 @@ module RSyntaxTree
|
|
|
435
480
|
!OFF.include?(value.to_s.strip.downcase)
|
|
436
481
|
end
|
|
437
482
|
|
|
483
|
+
# Whether the input is nothing to draw. Whitespace alone counts: it
|
|
484
|
+
# leaves no label behind, and reporting it as a defect in the library
|
|
485
|
+
# told the caller their own fixable input was our fault. Asked without
|
|
486
|
+
# strip when the bytes are not valid UTF-8, because strip raises there
|
|
487
|
+
# and that input is not blank — it is malformed, which the caller
|
|
488
|
+
# learns further down.
|
|
489
|
+
def self.blank?(text)
|
|
490
|
+
s = text.to_s
|
|
491
|
+
return true if s.empty?
|
|
492
|
+
|
|
493
|
+
s.valid_encoding? && s.strip.empty?
|
|
494
|
+
end
|
|
495
|
+
|
|
438
496
|
def self.check_data(text, params = {})
|
|
439
|
-
raise RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false) if text
|
|
497
|
+
raise RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false) if blank?(text)
|
|
440
498
|
|
|
441
499
|
begin
|
|
442
500
|
StringParser.valid?(text)
|
|
@@ -510,7 +568,7 @@ module RSyntaxTree
|
|
|
510
568
|
def self.diagnose(text, params = {})
|
|
511
569
|
errors = []
|
|
512
570
|
note = nil
|
|
513
|
-
if text
|
|
571
|
+
if blank?(text)
|
|
514
572
|
errors << RSTError.new(+"Error: input text is empty", code: :empty_input, retryable: false)
|
|
515
573
|
else
|
|
516
574
|
begin
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yoichiro Hasebe
|
|
@@ -128,6 +128,7 @@ files:
|
|
|
128
128
|
- lib/rsyntaxtree/color_names.rb
|
|
129
129
|
- lib/rsyntaxtree/element.rb
|
|
130
130
|
- lib/rsyntaxtree/elementlist.rb
|
|
131
|
+
- lib/rsyntaxtree/escape.rb
|
|
131
132
|
- lib/rsyntaxtree/format_converter.rb
|
|
132
133
|
- lib/rsyntaxtree/json_graph.rb
|
|
133
134
|
- lib/rsyntaxtree/markup_parser.rb
|