nagoro 2009.05
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +446 -0
- data/MANIFEST +51 -0
- data/README.markdown +189 -0
- data/Rakefile +29 -0
- data/bin/nagoro +83 -0
- data/doc/COPYING +56 -0
- data/doc/GPL +340 -0
- data/doc/LEGAL +2 -0
- data/example/element/Html.nage +8 -0
- data/example/hello.nag +3 -0
- data/example/morpher.nag +23 -0
- data/lib/nagoro.rb +22 -0
- data/lib/nagoro/binding.rb +8 -0
- data/lib/nagoro/element.rb +46 -0
- data/lib/nagoro/pipe.rb +12 -0
- data/lib/nagoro/pipe/base.rb +56 -0
- data/lib/nagoro/pipe/compile.rb +30 -0
- data/lib/nagoro/pipe/element.rb +70 -0
- data/lib/nagoro/pipe/include.rb +36 -0
- data/lib/nagoro/pipe/instruction.rb +64 -0
- data/lib/nagoro/pipe/localization.rb +60 -0
- data/lib/nagoro/pipe/morph.rb +95 -0
- data/lib/nagoro/pipe/tidy.rb +49 -0
- data/lib/nagoro/scanner.rb +97 -0
- data/lib/nagoro/template.rb +89 -0
- data/lib/nagoro/tidy.rb +49 -0
- data/lib/nagoro/version.rb +3 -0
- data/nagoro.gemspec +28 -0
- data/spec/core_extensions.rb +33 -0
- data/spec/example/hello.rb +13 -0
- data/spec/helper.rb +19 -0
- data/spec/nagoro/listener/base.rb +19 -0
- data/spec/nagoro/pipe/compile.rb +31 -0
- data/spec/nagoro/pipe/element.rb +46 -0
- data/spec/nagoro/pipe/include.rb +17 -0
- data/spec/nagoro/pipe/instruction.rb +32 -0
- data/spec/nagoro/pipe/morph.rb +47 -0
- data/spec/nagoro/pipe/tidy.rb +23 -0
- data/spec/nagoro/template.rb +105 -0
- data/spec/nagoro/template/full.nag +26 -0
- data/spec/nagoro/template/hello.nag +1 -0
- data/tasks/bacon.rake +49 -0
- data/tasks/changelog.rake +18 -0
- data/tasks/gem.rake +22 -0
- data/tasks/gem_installer.rake +76 -0
- data/tasks/grancher.rake +12 -0
- data/tasks/install_dependencies.rake +6 -0
- data/tasks/manifest.rake +4 -0
- data/tasks/rcov.rake +19 -0
- data/tasks/release.rake +52 -0
- data/tasks/reversion.rake +8 -0
- metadata +105 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
module Nagoro
|
2
|
+
module Pipe
|
3
|
+
# Morph is a search and replace system based on parameters of opening
|
4
|
+
# tags.
|
5
|
+
#
|
6
|
+
# Available morphs are at the time of writing:
|
7
|
+
# each, filter, foreach, if, times, unless
|
8
|
+
#
|
9
|
+
# Please take care not to use multiple morphable parameters in one tag, due
|
10
|
+
# to restrictions of the implementation of REXML and libxml there is no way
|
11
|
+
# to find out which one came first and so the order would get messed up,
|
12
|
+
# leading to unpredictable results.
|
13
|
+
# You can, however, use any number of other parameter together with morphs.
|
14
|
+
#
|
15
|
+
# Example for each predefined morph:
|
16
|
+
# <a if="@address" href="#@address">#@address</a>
|
17
|
+
# # <?r if @address ?>
|
18
|
+
# # <a href="#@address">#@address</a>
|
19
|
+
# # <?r end ?>
|
20
|
+
#
|
21
|
+
# <a unless="logged_in?" href="/login">Login to your account</a>
|
22
|
+
# # <?r unless logged_in? ?>
|
23
|
+
# # <a href="/login">Login to your account
|
24
|
+
# # <?r end ?>
|
25
|
+
#
|
26
|
+
# <div foreach="tag in @tags" class="tag">#{tag}</div>
|
27
|
+
# # <?r for tag in @tags ?>
|
28
|
+
# # <div class="tag">#{tag}</div>
|
29
|
+
# # <?r end ?>
|
30
|
+
#
|
31
|
+
# <p filter="emoticonize">Hello there :)</p>
|
32
|
+
# # <p>#{emoticonize(%<Hello there :)>)}</p>
|
33
|
+
#
|
34
|
+
# <div times="10" class="shout">I am innocent!</div>
|
35
|
+
# # <?r 10.times do |_t| ?>
|
36
|
+
# # <div class="shout">I am innocent!</div>
|
37
|
+
# # <?r end ?>
|
38
|
+
#
|
39
|
+
# <div each="@users">#{_e}</div>
|
40
|
+
# # <?r @users.each do |_e| ?>
|
41
|
+
# # <div>#{_e}</div>
|
42
|
+
# # <?r end ?>
|
43
|
+
#
|
44
|
+
# How to add new morphs:
|
45
|
+
# Nagoro::Pipe::Morph['until'] = [
|
46
|
+
# '<?r %morph %expression ?>', '<?r end ?>' ]
|
47
|
+
|
48
|
+
class Morph < Base
|
49
|
+
MORPHS = {
|
50
|
+
'each' => [ '<?r %expression.%morph do |_e| ?>', '<?r end ?>' ],
|
51
|
+
'times' => [ '<?r %expression.%morph do |_t| ?>', '<?r end ?>' ],
|
52
|
+
'filter' => [ '<?o %expression(%<', '>) ?>' ],
|
53
|
+
'if' => [ '<?r %morph %expression ?>', '<?r end ?>' ],
|
54
|
+
'unless' => [ '<?r %morph %expression ?>', '<?r end ?>' ],
|
55
|
+
'foreach' => [ '<?r for %expression ?>', '<?r end ?>' ],
|
56
|
+
}
|
57
|
+
|
58
|
+
def tag_start(tag, original_attrs, value_attrs)
|
59
|
+
morphs = value_attrs.keys & MORPHS.keys
|
60
|
+
return super if morphs.empty?
|
61
|
+
|
62
|
+
if morphs.size > 1
|
63
|
+
raise "Cannot transform multiple morphs: #{value_attrs.inspect} in <#{tag}>"
|
64
|
+
elsif morphs.size == 1
|
65
|
+
morph = morphs.first
|
66
|
+
value = value_attrs.delete(morph)
|
67
|
+
original_attrs.delete(morph)
|
68
|
+
open = MORPHS[morph][0]
|
69
|
+
|
70
|
+
append open.gsub(/%morph/, morph).gsub(/%expression/, value)
|
71
|
+
@stack << [tag, morph]
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def tag_end(tag)
|
77
|
+
super
|
78
|
+
prev, morph = @stack.last
|
79
|
+
return unless prev == tag
|
80
|
+
append(MORPHS[morph][1])
|
81
|
+
@stack.pop
|
82
|
+
end
|
83
|
+
|
84
|
+
class << self
|
85
|
+
def []=(key, value)
|
86
|
+
MORPHS[key] = value
|
87
|
+
end
|
88
|
+
|
89
|
+
def [](key)
|
90
|
+
MORPHS[key]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Nagoro
|
4
|
+
module Pipe
|
5
|
+
# Small wrapper that pipes the template through tidy.
|
6
|
+
#
|
7
|
+
# For configuration see the {Tidy::FLAGS}, it's an array of arguments
|
8
|
+
# passed to the tidy command.
|
9
|
+
#
|
10
|
+
# We are not using the ruby tidy library to avoid memory-leaks and
|
11
|
+
# dependencies. Please make sure you do have a binary called `tidy` in your
|
12
|
+
# `PATH` before using this library.
|
13
|
+
#
|
14
|
+
# This pipe relies on open3 from the standard library.
|
15
|
+
#
|
16
|
+
# == Regarding Windows
|
17
|
+
#
|
18
|
+
# There have been numerous reports that open3 doesn't work on Windows,
|
19
|
+
# since it relies on Kernel#fork, as syscall that is not supported on that
|
20
|
+
# platform.
|
21
|
+
#
|
22
|
+
# Two possible solutions exist:
|
23
|
+
# * There is a win32-popen implementation in RAA
|
24
|
+
# * Kernel#fork works in cygwin.
|
25
|
+
#
|
26
|
+
# I don't have a copy of windows, so it's hard for me to try either of
|
27
|
+
# these options.
|
28
|
+
# Please send me a patch if you find a cross-platform way of implementing
|
29
|
+
# this functionality.
|
30
|
+
class Tidy < Base
|
31
|
+
# Possible flags can be found in `tidy -help` and `tidy -help-config`
|
32
|
+
FLAGS = %w[-utf8 -asxhtml -quiet -indent --tidy-mark no]
|
33
|
+
|
34
|
+
def result
|
35
|
+
@scanner.stream
|
36
|
+
tidy(@body.join)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Pipe given +string+ through the tidy binary.
|
40
|
+
#
|
41
|
+
# @param [#to_s] string
|
42
|
+
# @return [String]
|
43
|
+
# @author manveru
|
44
|
+
def tidy(string)
|
45
|
+
Nagoro::Tidy.tidy(string, FLAGS)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Nagoro
|
4
|
+
class Scanner < StringScanner
|
5
|
+
TEXT = /[^<>]+/m
|
6
|
+
DOCTYPE = /<!DOCTYPE([^>]+)>/m
|
7
|
+
|
8
|
+
TAG_START = /<([^\s>]+)/
|
9
|
+
TAG_END = /<\/([^>]*)>/
|
10
|
+
TAG_OPEN_END = /\s*>/
|
11
|
+
TAG_CLOSING_END = /\s*\/>/
|
12
|
+
TAG_PARAMETER = /\s*([^\s]*)=((['"])(.*?)\3)/um
|
13
|
+
|
14
|
+
INSTRUCTION_START = /<\?(\S+)/
|
15
|
+
INSTRUCTION_END = /(.*?)(\?>)/um
|
16
|
+
|
17
|
+
RUBY_INTERP_START = /\s*#\{/m
|
18
|
+
RUBY_INTERP_TEXT = /[^\{\}]+/m
|
19
|
+
RUBY_INTERP_NEST = /\{[^\}]*\}/m
|
20
|
+
RUBY_INTERP_END = /(?=\})/
|
21
|
+
|
22
|
+
COMMENT = /<!--.*?-->/m
|
23
|
+
|
24
|
+
def initialize(string, callback)
|
25
|
+
@callback = callback
|
26
|
+
super(string)
|
27
|
+
end
|
28
|
+
|
29
|
+
def stream
|
30
|
+
until eos?
|
31
|
+
pos = self.pos
|
32
|
+
run
|
33
|
+
raise(Stuck, "Scanner didn't move: %p" % self) if pos == self.pos
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def run
|
38
|
+
if scan(DOCTYPE ); doctype(self[1])
|
39
|
+
elsif scan(INSTRUCTION_START); instruction(self[1])
|
40
|
+
elsif scan(COMMENT ); text(matched)
|
41
|
+
elsif scan(TAG_END ); tag_end(self[1])
|
42
|
+
elsif scan(RUBY_INTERP_START); ruby_interp(matched)
|
43
|
+
elsif scan(TAG_START ); tag_start(self[1])
|
44
|
+
elsif scan(TEXT ); text(matched)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def instruction(name)
|
49
|
+
scan(INSTRUCTION_END)
|
50
|
+
@callback.instruction(name, self[1])
|
51
|
+
end
|
52
|
+
|
53
|
+
def ruby_interp(string)
|
54
|
+
done = false
|
55
|
+
|
56
|
+
until done
|
57
|
+
if scan(RUBY_INTERP_TEXT)
|
58
|
+
string << matched
|
59
|
+
elsif scan(RUBY_INTERP_NEST)
|
60
|
+
string << matched
|
61
|
+
elsif scan(RUBY_INTERP_END)
|
62
|
+
done = true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
@callback.text(string)
|
67
|
+
end
|
68
|
+
|
69
|
+
def tag_start(name)
|
70
|
+
original_attrs = {}
|
71
|
+
value_attrs = {}
|
72
|
+
|
73
|
+
while scan(TAG_PARAMETER)
|
74
|
+
original_attrs[self[1]] = self[2] # <a href="foo"> gives 'href'=>'"foo"'
|
75
|
+
value_attrs[ self[1]] = self[4] # <a href="foo"> gives 'href'=>'foo'
|
76
|
+
end
|
77
|
+
|
78
|
+
@callback.tag_start(name, original_attrs, value_attrs)
|
79
|
+
return @callback.tag_end(name) if scan(TAG_CLOSING_END)
|
80
|
+
scan(TAG_OPEN_END)
|
81
|
+
end
|
82
|
+
|
83
|
+
def tag_end(name)
|
84
|
+
@callback.tag_end(name)
|
85
|
+
end
|
86
|
+
|
87
|
+
def text(string)
|
88
|
+
@callback.text(string)
|
89
|
+
end
|
90
|
+
|
91
|
+
def doctype(string)
|
92
|
+
@callback.doctype(string)
|
93
|
+
end
|
94
|
+
|
95
|
+
class Stuck < Error; end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'nagoro/scanner'
|
2
|
+
require 'nagoro/binding'
|
3
|
+
require 'nagoro/element'
|
4
|
+
require 'nagoro/pipe'
|
5
|
+
|
6
|
+
module Nagoro
|
7
|
+
DEFAULT_PIPES = [ Pipe::Element, Pipe::Morph, Pipe::Include,
|
8
|
+
Pipe::Instruction, Pipe::Compile ]
|
9
|
+
DEFAULT_FILE = '<nagoro eval>'
|
10
|
+
DEFAULT_LINE = 1
|
11
|
+
|
12
|
+
class Template
|
13
|
+
def self.[](*pipes)
|
14
|
+
new(:pipes => pipes.flatten)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :binding, :file, :pipes, :compiled
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
parse_option(options)
|
21
|
+
@compiled = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_option(options = {})
|
25
|
+
@binding = options.fetch(:binding, BindingProvider.binding)
|
26
|
+
@file = options[:filename] || DEFAULT_FILE
|
27
|
+
@line = options[:line] || DEFAULT_LINE
|
28
|
+
@pipes = options[:pipes] || DEFAULT_PIPES
|
29
|
+
@variables = options[:variables]
|
30
|
+
end
|
31
|
+
|
32
|
+
def compile(io, options = {})
|
33
|
+
parse_option(options) unless options.empty?
|
34
|
+
|
35
|
+
case io
|
36
|
+
when String
|
37
|
+
io = File.read(io) if io.size < 1024 and File.file?(io)
|
38
|
+
@compiled = pipeline(io)
|
39
|
+
when StringIO, IO
|
40
|
+
@compiled = pipeline(io.read)
|
41
|
+
else
|
42
|
+
raise(ArgumentError, "Cannot compile %p" % io)
|
43
|
+
end
|
44
|
+
|
45
|
+
return self
|
46
|
+
end
|
47
|
+
|
48
|
+
# use inject?
|
49
|
+
def pipeline(io)
|
50
|
+
@pipes.each do |pipe|
|
51
|
+
if pipe.respond_to?(:new)
|
52
|
+
io = pipe.new(io).result
|
53
|
+
else
|
54
|
+
io = Pipe.const_get(pipe).new(io).result
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
return io
|
59
|
+
rescue Scanner::Stuck => exception
|
60
|
+
exception.message << " In: #{@file}"
|
61
|
+
puts exception
|
62
|
+
''
|
63
|
+
end
|
64
|
+
|
65
|
+
def result(options = {})
|
66
|
+
parse_option(options) unless options.empty?
|
67
|
+
|
68
|
+
if vars = @variables
|
69
|
+
obj = eval('self', @binding)
|
70
|
+
obj.instance_variable_set('@_nagoro_ivs', vars)
|
71
|
+
eval(%q(
|
72
|
+
@_nagoro_ivs.each{|key, value| instance_variable_set("@#{key}", value) }
|
73
|
+
), @binding)
|
74
|
+
end
|
75
|
+
|
76
|
+
eval(@compiled, @binding, @file, @line).strip
|
77
|
+
end
|
78
|
+
|
79
|
+
def tidy_result(options = {})
|
80
|
+
final = result(options)
|
81
|
+
flags = options.fetch(:flags, Tidy::FLAGS)
|
82
|
+
Tidy.tidy(final, flags).strip
|
83
|
+
end
|
84
|
+
|
85
|
+
def render(io, options = {})
|
86
|
+
compile(io, options).result
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/nagoro/tidy.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Nagoro
|
4
|
+
# Small wrapper that pipes the template through tidy.
|
5
|
+
#
|
6
|
+
# For configuration see the {Tidy::FLAGS}, it's an array of arguments
|
7
|
+
# passed to the tidy command.
|
8
|
+
#
|
9
|
+
# We are not using the ruby tidy library to avoid memory-leaks and
|
10
|
+
# dependencies. Please make sure you do have a binary called `tidy` in your
|
11
|
+
# `PATH` before using this library.
|
12
|
+
#
|
13
|
+
# This pipe relies on open3 from the standard library.
|
14
|
+
#
|
15
|
+
# == Regarding Windows
|
16
|
+
#
|
17
|
+
# There have been numerous reports that open3 doesn't work on Windows,
|
18
|
+
# since it relies on Kernel#fork, as syscall that is not supported on that
|
19
|
+
# platform.
|
20
|
+
#
|
21
|
+
# Two possible solutions exist:
|
22
|
+
# * There is a win32-popen implementation in RAA
|
23
|
+
# * Kernel#fork works in cygwin.
|
24
|
+
#
|
25
|
+
# I don't have a copy of windows, so it's hard for me to try either of
|
26
|
+
# these options.
|
27
|
+
# Please send me a patch if you find a cross-platform way of implementing
|
28
|
+
# this functionality.
|
29
|
+
|
30
|
+
module Tidy
|
31
|
+
# Possible flags can be found in `tidy -help` and `tidy -help-config`
|
32
|
+
FLAGS = %w[-utf8 -asxhtml -quiet -indent --tidy-mark no]
|
33
|
+
|
34
|
+
module_function
|
35
|
+
|
36
|
+
# Pipe given +string+ through the tidy binary.
|
37
|
+
#
|
38
|
+
# @param [#to_s] string
|
39
|
+
# @return [String]
|
40
|
+
# @author manveru
|
41
|
+
def tidy(string, flags = FLAGS)
|
42
|
+
Open3.popen3('tidy', *flags) do |stdin, stdout, stderr|
|
43
|
+
stdin.puts(string)
|
44
|
+
stdin.close
|
45
|
+
return stdout.read
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/nagoro.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{nagoro}
|
5
|
+
s.version = "2009.05"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Michael 'manveru' Fellinger"]
|
9
|
+
s.date = %q{2009-05-07}
|
10
|
+
s.description = %q{An extendible and fast templating engine in pure ruby.}
|
11
|
+
s.email = %q{m.fellinger@gmail.com}
|
12
|
+
s.files = ["CHANGELOG", "MANIFEST", "README.markdown", "Rakefile", "bin/nagoro", "doc/COPYING", "doc/GPL", "doc/LEGAL", "example/element/Html.nage", "example/hello.nag", "example/morpher.nag", "lib/nagoro.rb", "lib/nagoro/binding.rb", "lib/nagoro/element.rb", "lib/nagoro/pipe.rb", "lib/nagoro/pipe/base.rb", "lib/nagoro/pipe/compile.rb", "lib/nagoro/pipe/element.rb", "lib/nagoro/pipe/include.rb", "lib/nagoro/pipe/instruction.rb", "lib/nagoro/pipe/localization.rb", "lib/nagoro/pipe/morph.rb", "lib/nagoro/pipe/tidy.rb", "lib/nagoro/scanner.rb", "lib/nagoro/template.rb", "lib/nagoro/tidy.rb", "lib/nagoro/version.rb", "nagoro.gemspec", "spec/core_extensions.rb", "spec/example/hello.rb", "spec/helper.rb", "spec/nagoro/listener/base.rb", "spec/nagoro/pipe/compile.rb", "spec/nagoro/pipe/element.rb", "spec/nagoro/pipe/include.rb", "spec/nagoro/pipe/instruction.rb", "spec/nagoro/pipe/morph.rb", "spec/nagoro/pipe/tidy.rb", "spec/nagoro/template.rb", "spec/nagoro/template/full.nag", "spec/nagoro/template/hello.nag", "tasks/bacon.rake", "tasks/changelog.rake", "tasks/gem.rake", "tasks/gem_installer.rake", "tasks/grancher.rake", "tasks/install_dependencies.rake", "tasks/manifest.rake", "tasks/rcov.rake", "tasks/release.rake", "tasks/reversion.rake"]
|
13
|
+
s.has_rdoc = true
|
14
|
+
s.homepage = %q{http://github.com/manveru/nagoro}
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.rubygems_version = %q{1.3.2}
|
17
|
+
s.summary = %q{An extendible and fast templating engine in pure ruby.}
|
18
|
+
|
19
|
+
if s.respond_to? :specification_version then
|
20
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
21
|
+
s.specification_version = 3
|
22
|
+
|
23
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
24
|
+
else
|
25
|
+
end
|
26
|
+
else
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Extensions for Kernel
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
# This is similar to +__FILE__+ and +__LINE__+, and returns a String
|
5
|
+
# representing the directory of the current file is.
|
6
|
+
# Unlike +__FILE__+ the path returned is absolute.
|
7
|
+
#
|
8
|
+
# This method is convenience for the
|
9
|
+
# File.expand_path(File.dirname(__FILE__))
|
10
|
+
# idiom.
|
11
|
+
#
|
12
|
+
unless defined?(__DIR__)
|
13
|
+
def __DIR__()
|
14
|
+
filename = caller[0][/(.*?):/, 1]
|
15
|
+
File.expand_path(File.dirname(filename))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Extensions for String
|
21
|
+
|
22
|
+
class String
|
23
|
+
|
24
|
+
# A convenient way to do File.join
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
# 'a' / 'b' # -> 'a/b'
|
28
|
+
# File.dirname(__FILE__) / 'bar' # -> "ramaze/snippets/string/bar"
|
29
|
+
|
30
|
+
def / obj
|
31
|
+
File.join(self, obj.to_s)
|
32
|
+
end
|
33
|
+
end
|