gherkin 0.0.3-universal-java-1.5 → 0.0.4-universal-java-1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -3
- data/README.rdoc +19 -0
- data/Rakefile +16 -15
- data/VERSION.yml +1 -1
- data/bin/gherkin +2 -2
- data/features/pretty_printer.feature +5 -2
- data/features/step_definitions/pretty_printer_steps.rb +6 -1
- data/gherkin.gemspec +16 -52
- data/lib/.gitignore +4 -2
- data/lib/gherkin/c_lexer.rb +3 -3
- data/lib/gherkin/core_ext/array.rb +5 -0
- data/lib/gherkin/lexer.rb +6 -5
- data/lib/gherkin/rb_lexer.rb +3 -2
- data/lib/gherkin/tools/pretty_printer.rb +10 -4
- data/nativegems.sh +5 -0
- data/ragel/lexer.c.rl.erb +28 -12
- data/ragel/lexer.java.rl.erb +5 -3
- data/ragel/lexer.rb.rl.erb +14 -15
- data/ragel/lexer_common.rl.erb +6 -6
- data/spec/gherkin/fixtures/1.feature +8 -0
- data/spec/gherkin/fixtures/complex.feature +2 -2
- data/spec/gherkin/shared/lexer_spec.rb +17 -4
- data/spec/gherkin/shared/py_string_spec.rb +12 -0
- data/tasks/bench.rake +28 -11
- data/tasks/compile.rake +70 -0
- data/tasks/ragel_task.rb +83 -0
- metadata +136 -165
- data/ext/gherkin_lexer/.gitignore +0 -6
- data/ext/gherkin_lexer/extconf.rb +0 -6
- data/tasks/ext.rake +0 -49
- data/tasks/ragel.rake +0 -94
data/ragel/lexer.java.rl.erb
CHANGED
@@ -7,7 +7,7 @@ import gherkin.Lexer;
|
|
7
7
|
import gherkin.Listener;
|
8
8
|
import gherkin.LexingError;
|
9
9
|
|
10
|
-
public class <%=
|
10
|
+
public class <%= @i18n.capitalize %> implements Lexer {
|
11
11
|
%%{
|
12
12
|
machine lexer;
|
13
13
|
alphtype byte;
|
@@ -72,10 +72,12 @@ public class <%= i18n_lexer_class_name %> implements Lexer {
|
|
72
72
|
|
73
73
|
action store_comment_content {
|
74
74
|
listener.comment(substring(data, contentStart, p).trim(), lineNumber);
|
75
|
+
keywordStart = -1;
|
75
76
|
}
|
76
77
|
|
77
78
|
action store_tag_content {
|
78
79
|
listener.tag(substring(data, contentStart, p).trim(), currentLine);
|
80
|
+
keywordStart = -1;
|
79
81
|
}
|
80
82
|
|
81
83
|
action inc_line_number {
|
@@ -134,12 +136,12 @@ public class <%= i18n_lexer_class_name %> implements Lexer {
|
|
134
136
|
}
|
135
137
|
}
|
136
138
|
|
137
|
-
include lexer_common "lexer_common.<%=
|
139
|
+
include lexer_common "lexer_common.<%= @i18n %>.rl";
|
138
140
|
}%%
|
139
141
|
|
140
142
|
private final Listener listener;
|
141
143
|
|
142
|
-
public <%=
|
144
|
+
public <%= @i18n.capitalize %>(Listener listener) {
|
143
145
|
this.listener = listener;
|
144
146
|
}
|
145
147
|
|
data/ragel/lexer.rb.rl.erb
CHANGED
@@ -1,12 +1,9 @@
|
|
1
|
+
require 'gherkin/core_ext/array'
|
2
|
+
|
1
3
|
module Gherkin
|
2
4
|
module RbLexer
|
3
|
-
class <%=
|
5
|
+
class <%= @i18n.capitalize %> #:nodoc:
|
4
6
|
%%{
|
5
|
-
# patterns:
|
6
|
-
# * data[start...end].pack("c*").strip_of_some_sort
|
7
|
-
# * changing the end point of the range according to next_keyword_start
|
8
|
-
# * methods taking the machine state because Ragel doesn't seem to know about ivars
|
9
|
-
|
10
7
|
machine lexer;
|
11
8
|
|
12
9
|
action begin_content {
|
@@ -24,7 +21,7 @@ module Gherkin
|
|
24
21
|
}
|
25
22
|
|
26
23
|
action store_pystring_content {
|
27
|
-
con = unindent(@start_col, data[@content_start...@next_keyword_start-1].
|
24
|
+
con = unindent(@start_col, data[@content_start...@next_keyword_start-1].utf8_pack("c*").sub(/(\r?\n)?( )*\Z/, ''))
|
28
25
|
@listener.py_string(con, @current_line)
|
29
26
|
}
|
30
27
|
|
@@ -59,18 +56,20 @@ module Gherkin
|
|
59
56
|
}
|
60
57
|
|
61
58
|
action store_step_content {
|
62
|
-
con = data[@content_start...p].
|
59
|
+
con = data[@content_start...p].utf8_pack("c*").strip
|
63
60
|
@listener.step(@keyword, con, @current_line)
|
64
61
|
}
|
65
62
|
|
66
63
|
action store_comment_content {
|
67
|
-
con = data[@content_start...p].
|
64
|
+
con = data[@content_start...p].utf8_pack("c*").strip
|
68
65
|
@listener.comment(con, @line_number)
|
66
|
+
@keyword_start = nil
|
69
67
|
}
|
70
68
|
|
71
69
|
action store_tag_content {
|
72
|
-
con = data[@content_start...p].
|
70
|
+
con = data[@content_start...p].utf8_pack("c*").strip
|
73
71
|
@listener.tag(con, @current_line)
|
72
|
+
@keyword_start = nil
|
74
73
|
}
|
75
74
|
|
76
75
|
action inc_line_number {
|
@@ -86,7 +85,7 @@ module Gherkin
|
|
86
85
|
}
|
87
86
|
|
88
87
|
action end_keyword {
|
89
|
-
@keyword = data[@keyword_start...p].
|
88
|
+
@keyword = data[@keyword_start...p].utf8_pack("c*").sub(/:$/,'').strip
|
90
89
|
@keyword_start = nil
|
91
90
|
}
|
92
91
|
|
@@ -109,7 +108,7 @@ module Gherkin
|
|
109
108
|
}
|
110
109
|
|
111
110
|
action store_cell_content {
|
112
|
-
con = data[@content_start...p].
|
111
|
+
con = data[@content_start...p].utf8_pack("c*").strip
|
113
112
|
current_row << con
|
114
113
|
}
|
115
114
|
|
@@ -130,7 +129,7 @@ module Gherkin
|
|
130
129
|
end
|
131
130
|
}
|
132
131
|
|
133
|
-
include lexer_common "lexer_common.<%=
|
132
|
+
include lexer_common "lexer_common.<%= @i18n %>.rl";
|
134
133
|
}%%
|
135
134
|
|
136
135
|
def initialize(listener)
|
@@ -159,13 +158,13 @@ module Gherkin
|
|
159
158
|
|
160
159
|
def store_keyword_content(event, data, p, eof)
|
161
160
|
end_point = (!@next_keyword_start or (p == eof)) ? p : @next_keyword_start
|
162
|
-
con = yield data[@content_start...end_point].
|
161
|
+
con = yield data[@content_start...end_point].utf8_pack("c*")
|
163
162
|
@listener.send(event, @keyword, con, @current_line)
|
164
163
|
end
|
165
164
|
|
166
165
|
def current_line_content(data, p)
|
167
166
|
rest = data[@last_newline..-1]
|
168
|
-
rest[0..rest.index(10)||-1].
|
167
|
+
rest[0..rest.index(10)||-1].utf8_pack("c*").strip
|
169
168
|
end
|
170
169
|
end
|
171
170
|
end
|
data/ragel/lexer_common.rl.erb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
machine lexer_common;
|
3
3
|
|
4
4
|
# Language specific
|
5
|
-
I18N_Feature = <%=
|
6
|
-
I18N_Background = <%=
|
7
|
-
I18N_ScenarioOutline = <%=
|
8
|
-
I18N_Scenario = <%=
|
9
|
-
I18N_Step = (<%=
|
10
|
-
I18N_Examples = <%=
|
5
|
+
I18N_Feature = <%= keywords['feature'] %> >start_keyword %end_keyword;
|
6
|
+
I18N_Background = <%= keywords['background'] %> >start_keyword %end_keyword;
|
7
|
+
I18N_ScenarioOutline = <%= keywords['scenario_outline'] %> >start_keyword %end_keyword;
|
8
|
+
I18N_Scenario = <%= keywords['scenario'] %> >start_keyword %end_keyword;
|
9
|
+
I18N_Step = (<%= keywords['given'] %> | <%= keywords['when'] %> | <%= keywords['and'] %> | <%= keywords['then'] %> | <%= keywords['but'] %>) >start_keyword %end_keyword;
|
10
|
+
I18N_Examples = <%= keywords['examples'] %> >start_keyword %end_keyword;
|
11
11
|
|
12
12
|
EOF = '%_FEATURE_END_%'; # Explicit EOF added before scanning begins
|
13
13
|
EOL = ('\r'? '\n') @inc_line_number @last_newline;
|
@@ -1,6 +1,6 @@
|
|
1
1
|
#Comment on line 1
|
2
|
+
#Comment on line 2
|
2
3
|
@tag1 @tag2
|
3
|
-
#Comment on line 3
|
4
4
|
Feature: Feature Text
|
5
5
|
In order to test multiline forms
|
6
6
|
As a ragel writer
|
@@ -40,4 +40,4 @@ Feature: Feature Text
|
|
40
40
|
"""
|
41
41
|
Makes Homer something something
|
42
42
|
"""
|
43
|
-
Then crazy
|
43
|
+
Then crazy
|
@@ -362,15 +362,28 @@ Given I am a step
|
|
362
362
|
]
|
363
363
|
end
|
364
364
|
end
|
365
|
-
|
365
|
+
|
366
|
+
describe "Comment or tag between Feature elements where previous narrative starts with same letter as a keyword" do
|
367
|
+
it "should lex this feature properly" do
|
368
|
+
scan_file("1.feature")
|
369
|
+
@listener.to_sexp.should == [
|
370
|
+
[:feature, "Feature", "Logging in\nSo that I can be myself", 1],
|
371
|
+
[:comment, "# Comment", 3],
|
372
|
+
[:scenario, "Scenario", "Anonymous user can get a login form.\nScenery here", 4],
|
373
|
+
[:tag, "tag", 7],
|
374
|
+
[:scenario, "Scenario", "Another one", 8]
|
375
|
+
]
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
366
379
|
describe "A complex feature with tags, comments, multiple scenarios, and multiple steps and tables" do
|
367
380
|
it "should find things in the right order" do
|
368
381
|
scan_file("complex.feature")
|
369
382
|
@listener.to_sexp.should == [
|
370
383
|
[:comment, "#Comment on line 1", 1],
|
371
|
-
[:
|
372
|
-
[:tag, "
|
373
|
-
[:
|
384
|
+
[:comment, "#Comment on line 2", 2],
|
385
|
+
[:tag, "tag1", 3],
|
386
|
+
[:tag, "tag2", 3],
|
374
387
|
[:feature, "Feature", "Feature Text\nIn order to test multiline forms\nAs a ragel writer\nI need to check for complex combinations", 4],
|
375
388
|
[:comment, "#Comment on line 9", 9],
|
376
389
|
[:comment, "#Comment on line 11", 11],
|
@@ -107,6 +107,18 @@ EOS
|
|
107
107
|
@listener.should_receive(:py_string).with(" Line one", 1)
|
108
108
|
@lexer.scan(str)
|
109
109
|
end
|
110
|
+
|
111
|
+
it "should preserve the last newline(s) at the end of a py_string" do
|
112
|
+
str = <<EOS
|
113
|
+
"""
|
114
|
+
PyString text
|
115
|
+
|
116
|
+
|
117
|
+
"""
|
118
|
+
EOS
|
119
|
+
@listener.should_receive(:py_string).with("PyString text\n\n",1)
|
120
|
+
@lexer.scan(str)
|
121
|
+
end
|
110
122
|
end
|
111
123
|
end
|
112
124
|
end
|
data/tasks/bench.rake
CHANGED
@@ -74,6 +74,7 @@ class Benchmarker
|
|
74
74
|
def report_all
|
75
75
|
Benchmark.bmbm do |x|
|
76
76
|
x.report("c_gherkin:") { run_c_gherkin }
|
77
|
+
x.report("c_gherkin_no_parser:") { run_c_gherkin_no_parser }
|
77
78
|
x.report("rb_gherkin:") { run_rb_gherkin }
|
78
79
|
x.report("cucumber:") { run_cucumber }
|
79
80
|
x.report("tt:") { run_tt }
|
@@ -93,32 +94,42 @@ class Benchmarker
|
|
93
94
|
def run_tt
|
94
95
|
require 'cucumber'
|
95
96
|
# Using Cucumber's Treetop lexer, but never calling #build to build the AST
|
96
|
-
lexer = Cucumber::Parser::NaturalLanguage.new(nil, 'en').
|
97
|
+
lexer = Cucumber::Parser::NaturalLanguage.new(nil, 'en').parser
|
97
98
|
@features.each do |file|
|
98
99
|
source = IO.read(file)
|
99
100
|
parse_tree = lexer.parse(source)
|
100
101
|
if parse_tree.nil?
|
101
|
-
raise Cucumber::
|
102
|
+
raise Cucumber::Parser::SyntaxError.new(lexer, file, 0)
|
102
103
|
end
|
103
104
|
end
|
104
105
|
end
|
105
106
|
|
106
107
|
def run_rb_gherkin
|
107
108
|
require 'gherkin'
|
109
|
+
require 'gherkin/rb_lexer'
|
108
110
|
require 'null_listener'
|
109
|
-
listener = NullListener.new
|
110
111
|
@features.each do |feature|
|
111
|
-
|
112
|
+
parser = Gherkin::Parser.new(NullListener.new, true, "root")
|
113
|
+
lexer = Gherkin::RbLexer['en'].new(parser)
|
112
114
|
lexer.scan(File.read(feature))
|
113
115
|
end
|
114
116
|
end
|
115
117
|
|
116
|
-
def run_c_gherkin
|
118
|
+
def run_c_gherkin
|
117
119
|
require 'gherkin'
|
118
120
|
require 'null_listener'
|
119
|
-
|
120
|
-
|
121
|
-
lexer = Gherkin::
|
121
|
+
@features.each do |feature|
|
122
|
+
parser = Gherkin::Parser.new(NullListener.new, true, "root")
|
123
|
+
lexer = Gherkin::CLexer['en'].new(parser)
|
124
|
+
lexer.scan(File.read(feature))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def run_c_gherkin_no_parser
|
129
|
+
require 'gherkin'
|
130
|
+
require 'null_listener'
|
131
|
+
@features.each do |feature|
|
132
|
+
lexer = Gherkin::CLexer['en'].new(NullListener.new)
|
122
133
|
lexer.scan(File.read(feature))
|
123
134
|
end
|
124
135
|
end
|
@@ -144,24 +155,30 @@ namespace :bench do
|
|
144
155
|
benchmarker.report("cucumber")
|
145
156
|
end
|
146
157
|
|
147
|
-
desc "Benchmark the Treetop
|
158
|
+
desc "Benchmark the Treetop parser with the features in tasks/bench/generated"
|
148
159
|
task :tt do
|
149
160
|
benchmarker = Benchmarker.new
|
150
161
|
benchmarker.report("tt")
|
151
162
|
end
|
152
163
|
|
153
|
-
desc "Benchmark the Ruby Gherkin lexer with the features in tasks/bench/generated"
|
164
|
+
desc "Benchmark the Ruby Gherkin lexer+parser with the features in tasks/bench/generated"
|
154
165
|
task :rb_gherkin do
|
155
166
|
benchmarker = Benchmarker.new
|
156
167
|
benchmarker.report("rb_gherkin")
|
157
168
|
end
|
158
169
|
|
159
|
-
desc "Benchmark the C Gherkin lexer with the features in tasks/bench/generated"
|
170
|
+
desc "Benchmark the C Gherkin lexer+parser with the features in tasks/bench/generated"
|
160
171
|
task :c_gherkin do
|
161
172
|
benchmarker = Benchmarker.new
|
162
173
|
benchmarker.report("c_gherkin")
|
163
174
|
end
|
164
175
|
|
176
|
+
desc "Benchmark the C Gherkin lexer (no parser) with the features in tasks/bench/generated"
|
177
|
+
task :c_gherkin_no_parser do
|
178
|
+
benchmarker = Benchmarker.new
|
179
|
+
benchmarker.report("c_gherkin_no_parser")
|
180
|
+
end
|
181
|
+
|
165
182
|
desc "Show basic statistics about the features in tasks/bench/generated"
|
166
183
|
task :stats do
|
167
184
|
["Feature", "Scenario", "Given"].each do |kw|
|
data/tasks/compile.rake
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/ragel_task'
|
2
|
+
|
3
|
+
CLEAN.include [
|
4
|
+
'**/*.{o,bundle,jar,so,obj,pdb,lib,def,exp,log}', 'ext',
|
5
|
+
'java/target',
|
6
|
+
'ragel/i18n/*.rl',
|
7
|
+
'lib/gherkin/rb_lexer/*.rb',
|
8
|
+
'ext/**/*.c',
|
9
|
+
'java/src/gherkin/lexer/*.java'
|
10
|
+
]
|
11
|
+
|
12
|
+
desc "Compile the Java extensions"
|
13
|
+
task :jar do
|
14
|
+
sh("ant -f java/build.xml")
|
15
|
+
end
|
16
|
+
|
17
|
+
YAML.load_file(File.dirname(__FILE__) + '/../lib/gherkin/i18n.yml').each do |i18n, keywords|
|
18
|
+
i18n = i18n.gsub(/[\s-]/, '')
|
19
|
+
|
20
|
+
java = RagelTask.new('java', i18n, keywords)
|
21
|
+
rb = RagelTask.new('rb', i18n, keywords)
|
22
|
+
|
23
|
+
task :jar => java.target
|
24
|
+
task :jar => rb.target
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'rake/extensiontask'
|
28
|
+
c = RagelTask.new('c', i18n, keywords)
|
29
|
+
|
30
|
+
extconf = "ext/gherkin_lexer_#{i18n}/extconf.rb"
|
31
|
+
|
32
|
+
file extconf do
|
33
|
+
FileUtils.mkdir(File.dirname(extconf)) unless File.directory?(File.dirname(extconf))
|
34
|
+
File.open(extconf, "w") do |io|
|
35
|
+
io.write(<<-EOF)
|
36
|
+
require 'mkmf'
|
37
|
+
dir_config("gherkin_lexer_#{i18n}")
|
38
|
+
have_library("c", "main")
|
39
|
+
create_makefile("gherkin_lexer_#{i18n}")
|
40
|
+
EOF
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Rake::ExtensionTask.new("gherkin_lexer_#{i18n}") do |ext|
|
45
|
+
if ENV['RUBY_CC_VERSION']
|
46
|
+
ext.cross_compile = true
|
47
|
+
ext.cross_platform = 'i386-mingw32'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
task :compile => c.target
|
52
|
+
task :compile => rb.target
|
53
|
+
|
54
|
+
# The way tasks are defined with compile:xxx (but without namespace) in rake-compiler forces us
|
55
|
+
# to use these hacks for setting up dependencies. Ugly!
|
56
|
+
Rake::Task["compile:gherkin_lexer_#{i18n}"].prerequisites.unshift(extconf)
|
57
|
+
Rake::Task["compile:gherkin_lexer_#{i18n}"].prerequisites.unshift(c.target)
|
58
|
+
Rake::Task["compile:gherkin_lexer_#{i18n}"].prerequisites.unshift(rb.target)
|
59
|
+
|
60
|
+
Rake::Task["compile"].prerequisites.unshift(extconf)
|
61
|
+
Rake::Task["compile"].prerequisites.unshift(c.target)
|
62
|
+
Rake::Task["compile"].prerequisites.unshift(rb.target)
|
63
|
+
rescue LoadError
|
64
|
+
unless defined?($c_warned)
|
65
|
+
warn "WARNING: Rake::ExtensionTask not installed. Skipping C compilation."
|
66
|
+
$c_warned = true
|
67
|
+
task :compile # no-op
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/tasks/ragel_task.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
class RagelTask
|
5
|
+
RL_OUTPUT_DIR = File.dirname(__FILE__) + "/../ragel/i18n"
|
6
|
+
|
7
|
+
def initialize(lang, i18n, keywords)
|
8
|
+
@lang = lang
|
9
|
+
@i18n = i18n
|
10
|
+
@keywords = keywords
|
11
|
+
define_tasks
|
12
|
+
end
|
13
|
+
|
14
|
+
def define_tasks
|
15
|
+
file target => [lang_ragel, common_ragel] do
|
16
|
+
mkdir_p(File.dirname(target)) unless File.directory?(File.dirname(target))
|
17
|
+
sh "ragel #{flags} #{lang_ragel} -o #{target}"
|
18
|
+
end
|
19
|
+
|
20
|
+
file lang_ragel => lang_erb do
|
21
|
+
impl = ERB.new(IO.read(lang_erb)).result(binding)
|
22
|
+
write(impl, lang_ragel)
|
23
|
+
end
|
24
|
+
|
25
|
+
file common_ragel => common_erb do
|
26
|
+
keywords = prep_keywords
|
27
|
+
common = ERB.new(IO.read(common_erb)).result(binding)
|
28
|
+
write(common, common_ragel)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def target
|
33
|
+
{
|
34
|
+
'c' => "ext/gherkin_lexer_#{@i18n}/gherkin_lexer_#{@i18n}.c",
|
35
|
+
'java' => "java/src/gherkin/lexer/#{@i18n.capitalize}.java",
|
36
|
+
'rb' => "lib/gherkin/rb_lexer/#{@i18n}.rb"
|
37
|
+
}[@lang]
|
38
|
+
end
|
39
|
+
|
40
|
+
def common_ragel
|
41
|
+
RL_OUTPUT_DIR + "/lexer_common.#{@i18n}.rl"
|
42
|
+
end
|
43
|
+
|
44
|
+
def common_erb
|
45
|
+
File.dirname(__FILE__) + '/../ragel/lexer_common.rl.erb'
|
46
|
+
end
|
47
|
+
|
48
|
+
def lang_ragel
|
49
|
+
RL_OUTPUT_DIR + "/#{@i18n}.#{@lang}.rl"
|
50
|
+
end
|
51
|
+
|
52
|
+
def lang_erb
|
53
|
+
File.dirname(__FILE__) + "/../ragel/lexer.#{@lang}.rl.erb"
|
54
|
+
end
|
55
|
+
|
56
|
+
def flags
|
57
|
+
{
|
58
|
+
'c' => '-C',
|
59
|
+
'java' => '-J',
|
60
|
+
'rb' => '-R'
|
61
|
+
}[@lang]
|
62
|
+
end
|
63
|
+
|
64
|
+
def prep_keywords
|
65
|
+
keywords = @keywords.dup
|
66
|
+
delimited_keywords = %w{feature background scenario scenario_outline examples}
|
67
|
+
bare_keywords = %w{given when then and but}
|
68
|
+
all_keywords = delimited_keywords + bare_keywords
|
69
|
+
|
70
|
+
all_keywords.each { |k| keywords[k] = keywords[k].split("|") }
|
71
|
+
delimited_keywords.each { |k| keywords[k].map! { |v| v += ':'} }
|
72
|
+
bare_keywords.each { |k| keywords[k].map! { |v| (v + ' ').sub(/< $/, '')} }
|
73
|
+
all_keywords.each { |k| keywords[k] = '("' + keywords[k].join('" | "') + '")' }
|
74
|
+
keywords
|
75
|
+
end
|
76
|
+
|
77
|
+
def write(content, filename)
|
78
|
+
mkdir_p(File.dirname(filename)) unless File.directory?(File.dirname(filename))
|
79
|
+
File.open(filename, "wb") do |file|
|
80
|
+
file.write(content)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|