rsyntaxtree 1.3.2 → 1.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/.github/FUNDING.yml +6 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +115 -52
- data/bin/rsyntaxtree +107 -19
- data/docs/_examples/054.md +22 -0
- data/docs/_examples/055.md +24 -0
- data/docs/_examples/056.md +27 -0
- data/docs/assets/img/054.png +0 -0
- data/docs/assets/img/055.png +0 -0
- data/docs/assets/img/056.png +0 -0
- data/docs/assets/svg/054.svg +50 -0
- data/docs/assets/svg/055.svg +53 -0
- data/docs/assets/svg/056.svg +73 -0
- data/docs/documentation.md +89 -0
- data/docs/documentation_ja.md +90 -1
- data/lib/rsyntaxtree/base_graph.rb +5 -5
- data/lib/rsyntaxtree/element.rb +3 -2
- data/lib/rsyntaxtree/elementlist.rb +3 -5
- data/lib/rsyntaxtree/format_converter.rb +65 -0
- data/lib/rsyntaxtree/markup_parser.rb +13 -2
- data/lib/rsyntaxtree/string_parser.rb +1 -1
- data/lib/rsyntaxtree/svg_graph.rb +9 -2
- data/lib/rsyntaxtree/tikz_generator.rb +131 -0
- data/lib/rsyntaxtree/utils.rb +1 -1
- data/lib/rsyntaxtree/version.rb +1 -1
- data/lib/rsyntaxtree.rb +42 -24
- data/rsyntaxtree.gemspec +2 -0
- data/syntree.svg +41 -0
- data/test/cli_test.rb +262 -0
- data/test/format_converter_test.rb +129 -0
- data/test/node_styling_test.rb +239 -0
- data/test/tikz_test.rb +89 -0
- metadata +51 -6
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#==========================
|
|
4
|
+
# tikz_generator.rb
|
|
5
|
+
#==========================
|
|
6
|
+
#
|
|
7
|
+
# Generates TikZ/forest LaTeX code from parsed tree elements
|
|
8
|
+
# Copyright (c) 2007-2026 Yoichiro Hasebe <yohasebe@gmail.com>
|
|
9
|
+
|
|
10
|
+
module RSyntaxTree
|
|
11
|
+
class TikZGenerator
|
|
12
|
+
LATEX_ESCAPE_MAP = {
|
|
13
|
+
'&' => '\\&',
|
|
14
|
+
'%' => '\\%',
|
|
15
|
+
'$' => '\\$',
|
|
16
|
+
'#' => '\\#',
|
|
17
|
+
'_' => '\\_',
|
|
18
|
+
'{' => '\\{',
|
|
19
|
+
'}' => '\\}',
|
|
20
|
+
'~' => '\\textasciitilde{}',
|
|
21
|
+
'^' => '\\textasciicircum{}'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def initialize(element_list, params)
|
|
25
|
+
@element_list = element_list
|
|
26
|
+
@params = params
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Generate TikZ forest code
|
|
30
|
+
# @param standalone [Boolean] whether to include LaTeX preamble
|
|
31
|
+
# @param font [String, nil] font name for XeLaTeX/fontspec (enables fontspec when specified)
|
|
32
|
+
# @return [String] TikZ/forest code
|
|
33
|
+
def generate(standalone: false, font: nil)
|
|
34
|
+
tree_code = generate_tree(1)
|
|
35
|
+
|
|
36
|
+
if standalone
|
|
37
|
+
generate_standalone(tree_code, font: font)
|
|
38
|
+
else
|
|
39
|
+
generate_forest_only(tree_code)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def generate_standalone(tree_code, font: nil)
|
|
46
|
+
if font
|
|
47
|
+
<<~LATEX
|
|
48
|
+
\\documentclass[border=10pt]{standalone}
|
|
49
|
+
\\usepackage{forest}
|
|
50
|
+
\\usepackage{fontspec}
|
|
51
|
+
\\setmainfont{#{font}}
|
|
52
|
+
|
|
53
|
+
\\begin{document}
|
|
54
|
+
#{generate_forest_only(tree_code)}
|
|
55
|
+
\\end{document}
|
|
56
|
+
LATEX
|
|
57
|
+
else
|
|
58
|
+
<<~LATEX
|
|
59
|
+
\\documentclass[border=10pt]{standalone}
|
|
60
|
+
\\usepackage{forest}
|
|
61
|
+
|
|
62
|
+
\\begin{document}
|
|
63
|
+
#{generate_forest_only(tree_code)}
|
|
64
|
+
\\end{document}
|
|
65
|
+
LATEX
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def generate_forest_only(tree_code)
|
|
70
|
+
<<~LATEX
|
|
71
|
+
\\begin{forest}
|
|
72
|
+
for tree={
|
|
73
|
+
parent anchor=south,
|
|
74
|
+
child anchor=north,
|
|
75
|
+
align=center,
|
|
76
|
+
base=top
|
|
77
|
+
}
|
|
78
|
+
#{tree_code}
|
|
79
|
+
\\end{forest}
|
|
80
|
+
LATEX
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Recursively generate tree structure
|
|
84
|
+
def generate_tree(element_id, indent = 0)
|
|
85
|
+
element = @element_list.get_id(element_id)
|
|
86
|
+
return "" unless element
|
|
87
|
+
|
|
88
|
+
label = extract_label(element)
|
|
89
|
+
escaped_label = escape_latex(label)
|
|
90
|
+
|
|
91
|
+
children = element.children
|
|
92
|
+
indent_str = " " * indent
|
|
93
|
+
|
|
94
|
+
if children.empty?
|
|
95
|
+
"#{indent_str}[#{escaped_label}]"
|
|
96
|
+
else
|
|
97
|
+
child_code = children.map { |child_id| generate_tree(child_id, indent + 1) }.join("\n")
|
|
98
|
+
"#{indent_str}[#{escaped_label}\n#{child_code}\n#{indent_str}]"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Extract plain text label from element content
|
|
103
|
+
def extract_label(element)
|
|
104
|
+
content = element.content
|
|
105
|
+
return "" if content.nil? || content.empty?
|
|
106
|
+
|
|
107
|
+
# content is an array of hashes with :type and :elements
|
|
108
|
+
texts = []
|
|
109
|
+
content.each do |line|
|
|
110
|
+
next unless line[:type] == :text
|
|
111
|
+
|
|
112
|
+
line[:elements].each do |el|
|
|
113
|
+
text = el[:text].to_s
|
|
114
|
+
# Remove RSyntaxTree-specific whitespace block
|
|
115
|
+
text = text.gsub("■", " ")
|
|
116
|
+
texts << text unless text.strip.empty?
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
texts.join(" ")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Escape LaTeX special characters
|
|
123
|
+
def escape_latex(text)
|
|
124
|
+
result = text.dup
|
|
125
|
+
LATEX_ESCAPE_MAP.each do |char, escaped|
|
|
126
|
+
result.gsub!(char, escaped)
|
|
127
|
+
end
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/rsyntaxtree/utils.rb
CHANGED
data/lib/rsyntaxtree/version.rb
CHANGED
data/lib/rsyntaxtree.rb
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Facade of rsyntaxtree library. When loaded by a driver script, it does all
|
|
8
8
|
# the necessary 'require' to use the library.
|
|
9
|
-
# Copyright (c) 2007-
|
|
9
|
+
# Copyright (c) 2007-2026 Yoichiro Hasebe <yohasebe@gmail.com>
|
|
10
10
|
|
|
11
11
|
FONT_DIR = File.expand_path(File.join(__dir__, "/../fonts"))
|
|
12
12
|
ETYPE_NODE = 1
|
|
@@ -24,7 +24,7 @@ DEFAULT_OPTS = {
|
|
|
24
24
|
linewidth: 1,
|
|
25
25
|
vheight: 2.0,
|
|
26
26
|
color: "modern",
|
|
27
|
-
symmetrize: "
|
|
27
|
+
symmetrize: "off",
|
|
28
28
|
transparent: "off",
|
|
29
29
|
polyline: "off",
|
|
30
30
|
hide_default_connectors: "off"
|
|
@@ -41,6 +41,7 @@ require_relative 'rsyntaxtree/utils'
|
|
|
41
41
|
require_relative 'rsyntaxtree/element'
|
|
42
42
|
require_relative 'rsyntaxtree/elementlist'
|
|
43
43
|
require_relative 'rsyntaxtree/svg_graph'
|
|
44
|
+
require_relative 'rsyntaxtree/tikz_generator'
|
|
44
45
|
require_relative 'rsyntaxtree/version'
|
|
45
46
|
require_relative 'rsyntaxtree/string_parser'
|
|
46
47
|
|
|
@@ -152,6 +153,9 @@ module RSyntaxTree
|
|
|
152
153
|
end
|
|
153
154
|
|
|
154
155
|
def draw_png(binary = false)
|
|
156
|
+
surface = nil
|
|
157
|
+
context = nil
|
|
158
|
+
b = nil
|
|
155
159
|
svg = draw_svg
|
|
156
160
|
rsvg = RSVG::Handle.new_from_data(svg)
|
|
157
161
|
dim = rsvg.dimensions
|
|
@@ -160,17 +164,19 @@ module RSyntaxTree
|
|
|
160
164
|
context.render_rsvg_handle(rsvg)
|
|
161
165
|
b = StringIO.new
|
|
162
166
|
surface.write_to_png(b)
|
|
163
|
-
|
|
164
|
-
# Clean up resources
|
|
165
|
-
b.close unless binary
|
|
166
|
-
surface.finish
|
|
167
|
-
context.destroy
|
|
168
|
-
result
|
|
167
|
+
binary ? b : b.string
|
|
169
168
|
rescue Cairo::InvalidSize
|
|
170
169
|
raise RSTError, +"Error: the result syntree is too big"
|
|
170
|
+
ensure
|
|
171
|
+
b&.close unless binary
|
|
172
|
+
surface&.finish
|
|
173
|
+
context&.destroy
|
|
171
174
|
end
|
|
172
175
|
|
|
173
176
|
def draw_pdf(binary = false)
|
|
177
|
+
surface = nil
|
|
178
|
+
context = nil
|
|
179
|
+
b = nil
|
|
174
180
|
b = StringIO.new
|
|
175
181
|
svg = draw_svg
|
|
176
182
|
rsvg = RSVG::Handle.new_from_data(svg)
|
|
@@ -179,13 +185,12 @@ module RSyntaxTree
|
|
|
179
185
|
context = Cairo::Context.new(surface)
|
|
180
186
|
context.render_rsvg_handle(rsvg)
|
|
181
187
|
surface.finish
|
|
182
|
-
|
|
183
|
-
# Clean up resources
|
|
184
|
-
b.close unless binary
|
|
185
|
-
context.destroy
|
|
186
|
-
result
|
|
188
|
+
binary ? b : b.string
|
|
187
189
|
rescue Cairo::InvalidSize
|
|
188
190
|
raise RSTError, +"Error: the result syntree is too big"
|
|
191
|
+
ensure
|
|
192
|
+
b&.close unless binary
|
|
193
|
+
context&.destroy
|
|
189
194
|
end
|
|
190
195
|
|
|
191
196
|
def draw_svg
|
|
@@ -195,18 +200,31 @@ module RSyntaxTree
|
|
|
195
200
|
graph.svg_data
|
|
196
201
|
end
|
|
197
202
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
image
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
im.format = @params[:format].upcase
|
|
206
|
-
end
|
|
207
|
-
# Clean up resources
|
|
208
|
-
image.destroy!
|
|
203
|
+
def draw_jpg
|
|
204
|
+
png_data = draw_png
|
|
205
|
+
images = Magick::Image.from_blob(png_data)
|
|
206
|
+
image = images.first
|
|
207
|
+
image.format = 'JPEG'
|
|
208
|
+
blob = image.to_blob
|
|
209
|
+
images.each(&:destroy!)
|
|
209
210
|
blob
|
|
210
211
|
end
|
|
212
|
+
|
|
213
|
+
def draw_gif
|
|
214
|
+
png_data = draw_png
|
|
215
|
+
images = Magick::Image.from_blob(png_data)
|
|
216
|
+
image = images.first
|
|
217
|
+
image.format = 'GIF'
|
|
218
|
+
blob = image.to_blob
|
|
219
|
+
images.each(&:destroy!)
|
|
220
|
+
blob
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def draw_tikz(standalone: false, font: nil)
|
|
224
|
+
sp = StringParser.new(@params[:data].gsub('&', '&'), @params[:fontset], @params[:fontsize], @global)
|
|
225
|
+
sp.parse
|
|
226
|
+
generator = TikZGenerator.new(sp.get_elementlist, @params)
|
|
227
|
+
generator.generate(standalone: standalone, font: font)
|
|
228
|
+
end
|
|
211
229
|
end
|
|
212
230
|
end
|
data/rsyntaxtree.gemspec
CHANGED
|
@@ -22,6 +22,8 @@ Gem::Specification.new do |s|
|
|
|
22
22
|
s.add_runtime_dependency "rmagick", ">= 4.3"
|
|
23
23
|
s.add_runtime_dependency "rsvg2"
|
|
24
24
|
|
|
25
|
+
s.add_development_dependency "minitest"
|
|
25
26
|
s.add_development_dependency "nokogiri"
|
|
27
|
+
s.add_development_dependency "rake"
|
|
26
28
|
s.add_development_dependency "yaml"
|
|
27
29
|
end
|
data/syntree.svg
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
3
|
+
<svg width="118.60000000000001" height="434.25" viewBox="-15.200000000000001, 0, 133.8, 445.5" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
4
|
+
<defs>
|
|
5
|
+
<marker id="arrow" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="15.200000000000001" markerHeight="15.200000000000001" orient="auto">
|
|
6
|
+
<path d="M 0 0 L 10 5 L 0 10" fill="#CC79A7"/>
|
|
7
|
+
</marker>
|
|
8
|
+
<marker id="arrowBackward" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="15.200000000000001" markerHeight="15.200000000000001" orient="auto">
|
|
9
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#CC79A7"/>
|
|
10
|
+
</marker>
|
|
11
|
+
<marker id="arrowForward" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="15.200000000000001" markerHeight="15.200000000000001" orient="auto">
|
|
12
|
+
<path d="M 10 0 L 0 5 L 10 10 z" fill="#CC79A7"/>
|
|
13
|
+
</marker>
|
|
14
|
+
<marker id="arrowBothways" markerUnits="userSpaceOnUse" viewBox="0 0 30 10" refX="15" refY="5" markerWidth="45.6" markerHeight="15.200000000000001" orient="auto">
|
|
15
|
+
<path d="M 0 5 L 10 0 L 10 5 L 20 5 L 20 0 L 30 5 L 20 10 L 20 5 L 10 5 L 10 10 z" fill="#CC79A7"/>
|
|
16
|
+
</marker>
|
|
17
|
+
<pattern id="hatchBlack" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
|
18
|
+
<line x1="0" y="0" x2="0" y2="10" stroke="black" stroke-width="4"></line>
|
|
19
|
+
</pattern>
|
|
20
|
+
<pattern id="hatchForNode" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
|
21
|
+
<line x1="0" y="0" x2="0" y2="10" stroke="#0072B2" stroke-width="4"></line>
|
|
22
|
+
</pattern>
|
|
23
|
+
<pattern id="hatchForLeaf" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
|
24
|
+
<line x1="0" y="0" x2="0" y2="10" stroke="#009E73" stroke-width="4"></line>
|
|
25
|
+
</pattern>
|
|
26
|
+
</defs>
|
|
27
|
+
<rect x="-15.200000000000001" y="0" width="133.8" height="445.5" stroke="none" fill="white" />"
|
|
28
|
+
<text white-space='pre' alignment-baseline='text-top' style='fill: #0072B2; storoke-width: 0; font-size: 32px;' x='42.7' y='67.5'><tspan x='42.7' y='67.5' style="" text-decoration="" font-family="'Noto Sans', 'Noto Sans JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif">S</tspan>
|
|
29
|
+
</text>
|
|
30
|
+
<text white-space='pre' alignment-baseline='text-top' style='fill: red; storoke-width: 0; font-size: 32px;' x='30.200000000000003' y='238.5'><tspan x='30.200000000000003' y='238.5' style="" text-decoration="" font-family="'Noto Sans', 'Noto Sans JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif">NP</tspan>
|
|
31
|
+
</text>
|
|
32
|
+
<text white-space='pre' alignment-baseline='text-top' style='fill: #009E73; storoke-width: 0; font-size: 32px;' x='15.200000000000003' y='409.5'><tspan x='15.200000000000003' y='409.5' style="" text-decoration="" font-family="'Noto Sans', 'Noto Sans JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif">hello</tspan>
|
|
33
|
+
</text>
|
|
34
|
+
<line style='fill: none; stroke:black; stroke-width:2; stroke-linejoin:round; stroke-linecap:round;' x1='51.7' y1='182.25' x2='51.7' y2='95.625' />
|
|
35
|
+
<line style='fill: none; stroke:black; stroke-width:2; stroke-linejoin:round; stroke-linecap:round;' x1='51.7' y1='353.25' x2='51.7' y2='266.625' />
|
|
36
|
+
<polyline style='stroke:red; stroke-width:2; fill:none; stroke-linejoin:round; stroke-linecap:round;'
|
|
37
|
+
points='30.200000000000003,187.875 22.6,187.875 22.6,261.0 30.200000000000003,261.0' />
|
|
38
|
+
|
|
39
|
+
<polyline style='stroke:red; stroke-width:2; fill:none; stroke-linejoin:round; stroke-linecap:round;'
|
|
40
|
+
points='73.20000000000002,187.875 80.80000000000001,187.875 80.80000000000001,261.0 73.20000000000002,261.0' />
|
|
41
|
+
</svg>
|
data/test/cli_test.rb
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "minitest/pride"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "open3"
|
|
7
|
+
|
|
8
|
+
class CLITest < Minitest::Test
|
|
9
|
+
BIN_PATH = File.expand_path("../bin/rsyntaxtree", __dir__)
|
|
10
|
+
RUBY = "ruby"
|
|
11
|
+
|
|
12
|
+
def setup
|
|
13
|
+
@tmpdir = Dir.mktmpdir
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def teardown
|
|
17
|
+
FileUtils.remove_entry(@tmpdir) if @tmpdir && Dir.exist?(@tmpdir)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Helper to run CLI and capture output (defined at bottom as private)
|
|
21
|
+
|
|
22
|
+
# ===================
|
|
23
|
+
# Feature 1: stdin support
|
|
24
|
+
# ===================
|
|
25
|
+
|
|
26
|
+
def test_stdin_input_generates_output
|
|
27
|
+
tree_data = "[S [NP test] [VP works]]"
|
|
28
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
29
|
+
|
|
30
|
+
result = run_cli("-f", "svg", "-o", @tmpdir, stdin_data: tree_data)
|
|
31
|
+
|
|
32
|
+
assert result[:status].success?, "CLI should succeed with stdin input: #{result[:stderr]}"
|
|
33
|
+
assert File.exist?(outfile), "Output file should be created"
|
|
34
|
+
assert File.size(outfile) > 0, "Output file should not be empty"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_stdin_input_with_pipe_simulation
|
|
38
|
+
tree_data = "[S [NP hello] [VP world]]"
|
|
39
|
+
outfile = File.join(@tmpdir, "syntree.png")
|
|
40
|
+
|
|
41
|
+
result = run_cli("-f", "png", "-o", @tmpdir, stdin_data: tree_data)
|
|
42
|
+
|
|
43
|
+
assert result[:status].success?, "CLI should succeed with piped input"
|
|
44
|
+
assert File.exist?(outfile), "PNG output file should be created"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_argument_takes_precedence_over_stdin
|
|
48
|
+
tree_data_stdin = "[S [NP stdin] [VP data]]"
|
|
49
|
+
tree_data_arg = "[S [NP arg] [VP data]]"
|
|
50
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
51
|
+
|
|
52
|
+
# When both arg and stdin provided, arg should win
|
|
53
|
+
result = run_cli("-f", "svg", "-o", @tmpdir, tree_data_arg, stdin_data: tree_data_stdin)
|
|
54
|
+
|
|
55
|
+
assert result[:status].success?, "CLI should succeed"
|
|
56
|
+
assert File.exist?(outfile), "Output file should be created"
|
|
57
|
+
content = File.read(outfile)
|
|
58
|
+
assert content.include?("arg"), "Output should contain data from argument, not stdin"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_file_input_still_works
|
|
62
|
+
tree_file = File.join(@tmpdir, "input.txt")
|
|
63
|
+
File.write(tree_file, "[S [NP file] [VP input]]")
|
|
64
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
65
|
+
|
|
66
|
+
result = run_cli("-f", "svg", "-o", @tmpdir, tree_file)
|
|
67
|
+
|
|
68
|
+
assert result[:status].success?, "CLI should succeed with file input"
|
|
69
|
+
assert File.exist?(outfile), "Output file should be created"
|
|
70
|
+
content = File.read(outfile)
|
|
71
|
+
assert content.include?("file"), "Output should contain data from file"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ===================
|
|
75
|
+
# Feature 2: Config file support
|
|
76
|
+
# ===================
|
|
77
|
+
|
|
78
|
+
def test_config_file_invalid_format_shows_error
|
|
79
|
+
config_file = File.join(@tmpdir, ".rsyntaxtreerc")
|
|
80
|
+
File.write(config_file, <<~YAML)
|
|
81
|
+
format: invalid_format
|
|
82
|
+
YAML
|
|
83
|
+
|
|
84
|
+
tree_data = "[S [NP test] [VP config]]"
|
|
85
|
+
result = run_cli("-o", @tmpdir, tree_data, chdir: @tmpdir)
|
|
86
|
+
|
|
87
|
+
refute result[:status].success?, "CLI should fail with invalid format in config"
|
|
88
|
+
assert result[:stderr].include?("format") || result[:stdout].include?("format"),
|
|
89
|
+
"Error should mention 'format'"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_config_file_invalid_fontsize_shows_error
|
|
93
|
+
config_file = File.join(@tmpdir, ".rsyntaxtreerc")
|
|
94
|
+
File.write(config_file, <<~YAML)
|
|
95
|
+
fontsize: 999
|
|
96
|
+
YAML
|
|
97
|
+
|
|
98
|
+
tree_data = "[S [NP test] [VP config]]"
|
|
99
|
+
result = run_cli("-o", @tmpdir, tree_data, chdir: @tmpdir)
|
|
100
|
+
|
|
101
|
+
refute result[:status].success?, "CLI should fail with invalid fontsize in config"
|
|
102
|
+
assert result[:stderr].include?("fontsize") || result[:stdout].include?("fontsize"),
|
|
103
|
+
"Error should mention 'fontsize'"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_config_file_invalid_color_shows_error
|
|
107
|
+
config_file = File.join(@tmpdir, ".rsyntaxtreerc")
|
|
108
|
+
File.write(config_file, <<~YAML)
|
|
109
|
+
color: rainbow
|
|
110
|
+
YAML
|
|
111
|
+
|
|
112
|
+
tree_data = "[S [NP test] [VP config]]"
|
|
113
|
+
result = run_cli("-o", @tmpdir, tree_data, chdir: @tmpdir)
|
|
114
|
+
|
|
115
|
+
refute result[:status].success?, "CLI should fail with invalid color in config"
|
|
116
|
+
assert result[:stderr].include?("color") || result[:stdout].include?("color"),
|
|
117
|
+
"Error should mention 'color'"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_config_file_unknown_key_shows_warning
|
|
121
|
+
config_file = File.join(@tmpdir, ".rsyntaxtreerc")
|
|
122
|
+
File.write(config_file, <<~YAML)
|
|
123
|
+
format: svg
|
|
124
|
+
unknown_option: value
|
|
125
|
+
YAML
|
|
126
|
+
|
|
127
|
+
tree_data = "[S [NP test] [VP config]]"
|
|
128
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
129
|
+
result = run_cli("-o", @tmpdir, tree_data, chdir: @tmpdir)
|
|
130
|
+
|
|
131
|
+
# Should succeed but warn about unknown key
|
|
132
|
+
assert result[:status].success?, "CLI should succeed with unknown key"
|
|
133
|
+
assert File.exist?(outfile), "Output should be created"
|
|
134
|
+
assert result[:stderr].include?("unknown_option") || result[:stdout].include?("unknown_option"),
|
|
135
|
+
"Should warn about unknown option"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_config_file_shows_path_in_error
|
|
139
|
+
config_file = File.join(@tmpdir, ".rsyntaxtreerc")
|
|
140
|
+
File.write(config_file, <<~YAML)
|
|
141
|
+
format: invalid
|
|
142
|
+
YAML
|
|
143
|
+
|
|
144
|
+
tree_data = "[S [NP test] [VP config]]"
|
|
145
|
+
result = run_cli("-o", @tmpdir, tree_data, chdir: @tmpdir)
|
|
146
|
+
|
|
147
|
+
refute result[:status].success?, "CLI should fail"
|
|
148
|
+
# Error message should include the config file path
|
|
149
|
+
assert result[:stderr].include?(".rsyntaxtreerc") || result[:stdout].include?(".rsyntaxtreerc"),
|
|
150
|
+
"Error should mention config file path"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_config_file_in_current_dir
|
|
154
|
+
# Create config file in tmpdir
|
|
155
|
+
config_file = File.join(@tmpdir, ".rsyntaxtreerc")
|
|
156
|
+
File.write(config_file, <<~YAML)
|
|
157
|
+
format: svg
|
|
158
|
+
color: off
|
|
159
|
+
fontsize: 20
|
|
160
|
+
YAML
|
|
161
|
+
|
|
162
|
+
tree_data = "[S [NP config] [VP test]]"
|
|
163
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
164
|
+
|
|
165
|
+
# Run from tmpdir with config
|
|
166
|
+
result = run_cli("-o", @tmpdir, tree_data, chdir: @tmpdir)
|
|
167
|
+
|
|
168
|
+
assert result[:status].success?, "CLI should succeed with config file: #{result[:stderr]}"
|
|
169
|
+
assert File.exist?(outfile), "Should output SVG as specified in config"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_cli_args_override_config
|
|
173
|
+
config_file = File.join(@tmpdir, ".rsyntaxtreerc")
|
|
174
|
+
File.write(config_file, <<~YAML)
|
|
175
|
+
format: svg
|
|
176
|
+
YAML
|
|
177
|
+
|
|
178
|
+
tree_data = "[S [NP override] [VP test]]"
|
|
179
|
+
outfile_png = File.join(@tmpdir, "syntree.png")
|
|
180
|
+
|
|
181
|
+
# CLI arg -f png should override config's svg
|
|
182
|
+
result = run_cli("-f", "png", "-o", @tmpdir, tree_data, chdir: @tmpdir)
|
|
183
|
+
|
|
184
|
+
assert result[:status].success?, "CLI should succeed"
|
|
185
|
+
assert File.exist?(outfile_png), "Should output PNG (CLI override)"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_config_file_with_custom_filename
|
|
189
|
+
config_file = File.join(@tmpdir, ".rsyntaxtreerc")
|
|
190
|
+
File.write(config_file, <<~YAML)
|
|
191
|
+
format: svg
|
|
192
|
+
outfilename: custom_tree
|
|
193
|
+
YAML
|
|
194
|
+
|
|
195
|
+
tree_data = "[S [NP custom] [VP name]]"
|
|
196
|
+
outfile = File.join(@tmpdir, "custom_tree.svg")
|
|
197
|
+
|
|
198
|
+
result = run_cli("-o", @tmpdir, tree_data, chdir: @tmpdir)
|
|
199
|
+
|
|
200
|
+
assert result[:status].success?, "CLI should succeed"
|
|
201
|
+
assert File.exist?(outfile), "Should use custom filename from config"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# ===================
|
|
205
|
+
# Feature 3: Penn TreeBank format
|
|
206
|
+
# ===================
|
|
207
|
+
|
|
208
|
+
def test_penn_treebank_input
|
|
209
|
+
tree_data = "(S (NP hello) (VP world))"
|
|
210
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
211
|
+
|
|
212
|
+
result = run_cli("-f", "svg", "-o", @tmpdir, tree_data)
|
|
213
|
+
|
|
214
|
+
assert result[:status].success?, "CLI should succeed with Penn TreeBank input: #{result[:stderr]}"
|
|
215
|
+
assert File.exist?(outfile), "Output file should be created"
|
|
216
|
+
content = File.read(outfile)
|
|
217
|
+
assert content.include?("hello"), "Output should contain tree content"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_penn_treebank_nested
|
|
221
|
+
tree_data = "(S (NP (Det the) (N dog)) (VP (V runs)))"
|
|
222
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
223
|
+
|
|
224
|
+
result = run_cli("-f", "svg", "-o", @tmpdir, tree_data)
|
|
225
|
+
|
|
226
|
+
assert result[:status].success?, "CLI should succeed with nested Penn TreeBank"
|
|
227
|
+
assert File.exist?(outfile), "Output file should be created"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_penn_treebank_from_file
|
|
231
|
+
tree_file = File.join(@tmpdir, "tree.penn")
|
|
232
|
+
File.write(tree_file, "(S (NP test) (VP file))")
|
|
233
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
234
|
+
|
|
235
|
+
result = run_cli("-f", "svg", "-o", @tmpdir, tree_file)
|
|
236
|
+
|
|
237
|
+
assert result[:status].success?, "CLI should succeed with Penn file input"
|
|
238
|
+
assert File.exist?(outfile), "Output file should be created"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_penn_treebank_from_stdin
|
|
242
|
+
tree_data = "(S (NP stdin) (VP penn))"
|
|
243
|
+
outfile = File.join(@tmpdir, "syntree.svg")
|
|
244
|
+
|
|
245
|
+
result = run_cli("-f", "svg", "-o", @tmpdir, stdin_data: tree_data)
|
|
246
|
+
|
|
247
|
+
assert result[:status].success?, "CLI should succeed with Penn from stdin"
|
|
248
|
+
assert File.exist?(outfile), "Output file should be created"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
# Updated helper to support chdir
|
|
254
|
+
def run_cli(*args, stdin_data: nil, chdir: nil)
|
|
255
|
+
cmd = [RUBY, BIN_PATH] + args
|
|
256
|
+
opts = {}
|
|
257
|
+
opts[:stdin_data] = stdin_data if stdin_data
|
|
258
|
+
opts[:chdir] = chdir if chdir
|
|
259
|
+
stdout, stderr, status = Open3.capture3(*cmd, **opts)
|
|
260
|
+
{ stdout: stdout, stderr: stderr, status: status }
|
|
261
|
+
end
|
|
262
|
+
end
|