herbgobbler 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/gobble +43 -0
- data/grammer/erb_grammer.treetop +256 -0
- data/lib/core/base_node.rb +29 -0
- data/lib/core/base_text_extractor.rb +19 -0
- data/lib/core/base_translation_store.rb +49 -0
- data/lib/core/core.rb +17 -0
- data/lib/core/double_quoted_text_node.rb +89 -0
- data/lib/core/erb_file.rb +159 -0
- data/lib/core/erb_string_non_text_content.rb +8 -0
- data/lib/core/i18n_key.rb +74 -0
- data/lib/core/ignorable_tag_node.rb +10 -0
- data/lib/core/method_call_node.rb +44 -0
- data/lib/core/node_processing.rb +26 -0
- data/lib/core/non_extracting_non_text_node.rb +8 -0
- data/lib/core/non_text_node.rb +32 -0
- data/lib/core/rails_text_extractor.rb +88 -0
- data/lib/core/rails_translation_store.rb +42 -0
- data/lib/core/text_extractor.rb +31 -0
- data/lib/core/text_node.rb +73 -0
- data/lib/herbgobbler.rb +7 -0
- data/lib/nodes/combindable_herb_non_text_node.rb +5 -0
- data/lib/nodes/herb_combined_node.rb +47 -0
- data/lib/nodes/herb_erb_text_call_node.rb +52 -0
- data/lib/nodes/herb_node_retaining_node.rb +37 -0
- data/lib/nodes/herb_node_retaining_non_text_node.rb +30 -0
- data/lib/nodes/herb_node_retaining_text_node.rb +107 -0
- data/lib/nodes/herb_non_text_node.rb +21 -0
- data/lib/nodes/herb_string_variable.rb +9 -0
- data/lib/nodes/herb_text_node.rb +23 -0
- data/lib/nodes/herb_white_space_text_node.rb +14 -0
- data/lib/nodes/nodes.rb +13 -0
- data/lib/nodes/rails_text_variable_node.rb +19 -0
- data/scripts/display_syntax_tree.rb +18 -0
- data/scripts/extract_text_from_erb.rb +12 -0
- data/scripts/extract_yml_from_erb.rb +12 -0
- data/scripts/rails_gobble_file.rb +37 -0
- data/scripts/save_result.rb +11 -0
- metadata +154 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
class I18nKey
|
2
|
+
|
3
|
+
DEFAULT_KEY_NAME = "key"
|
4
|
+
|
5
|
+
def initialize( text, key_store = [] )
|
6
|
+
@text = text
|
7
|
+
@key_store = key_store
|
8
|
+
@key_value = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def key_value
|
12
|
+
if( @key_value.nil? )
|
13
|
+
to_return = remove_html_tags( @text )
|
14
|
+
to_return = to_return.gsub( /[%{].*?[}]/, '' )
|
15
|
+
to_return = to_return.gsub( /[^a-zA-Z0-9]/, ' ' )
|
16
|
+
to_return = to_return.gsub( /[ ]+/, ' ' )
|
17
|
+
to_return = to_return.strip.downcase.chomp
|
18
|
+
to_return = to_return.gsub( / /, '_' )
|
19
|
+
to_return = cut_down_to_size( to_return )
|
20
|
+
to_return = ensure_no_duplicates( to_return )
|
21
|
+
self.key_value = to_return
|
22
|
+
end
|
23
|
+
@key_value
|
24
|
+
end
|
25
|
+
|
26
|
+
def key_value=(value)
|
27
|
+
@key_value = value
|
28
|
+
@key_store << value
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
key_value
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def cut_down_to_size( string_to_remove )
|
38
|
+
if( string_to_remove.length < 15 )
|
39
|
+
string_to_remove
|
40
|
+
else
|
41
|
+
if( string_to_remove.rindex( '_' ) )
|
42
|
+
cut_down_to_size( string_to_remove[0..(string_to_remove.rindex('_')-1) ] )
|
43
|
+
else
|
44
|
+
string_to_remove
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def exists_in_keystore?( value )
|
50
|
+
!(@key_store.index( value ).nil?)
|
51
|
+
end
|
52
|
+
|
53
|
+
def ensure_no_duplicates( key_value, incrementor = 1 )
|
54
|
+
if( key_value.strip.empty? )
|
55
|
+
ensure_no_duplicates( DEFAULT_KEY_NAME )
|
56
|
+
elsif( exists_in_keystore?( key_value ) )
|
57
|
+
incremented_value = "#{key_value}_#{incrementor}"
|
58
|
+
if( exists_in_keystore?( incremented_value ) )
|
59
|
+
ensure_no_duplicates( key_value, incrementor + 1 )
|
60
|
+
else
|
61
|
+
incremented_value
|
62
|
+
end
|
63
|
+
else
|
64
|
+
key_value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def remove_html_tags( string_to_remove )
|
69
|
+
string_to_remove.gsub( /<[\/]?[a-zA-Z]+[^<]*>/, '_')
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module MethodCallNode
|
2
|
+
include TextNode
|
3
|
+
|
4
|
+
def extract_text( text_extractor, node_tree, surrounding_nodes )
|
5
|
+
|
6
|
+
text_string = ''
|
7
|
+
self.elements.each do |node|
|
8
|
+
if( node.is_a?( TextNode ) )
|
9
|
+
translated_node = text_extractor.translate_text( node.text_value )
|
10
|
+
text_string << "(#{translated_node.text_value})"
|
11
|
+
else
|
12
|
+
text_string << node.text_value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if( surrounded_by_text? (surrounding_nodes) )
|
17
|
+
text_extractor.add_variable( generate_i18n_key( text_extractor, node_tree ).to_s, text_string.strip )
|
18
|
+
else
|
19
|
+
node_tree << HerbNonTextNode.new( "<%= #{text_string.strip} %>" )
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def surrounded_by_text?( nodes )
|
27
|
+
# if the surrounding nodes only contain myself and <%= and %> then this
|
28
|
+
# is not surrounded by text
|
29
|
+
count_non_whitespace_nodes( nodes ) > 3
|
30
|
+
end
|
31
|
+
|
32
|
+
def count_non_whitespace_nodes( nodes )
|
33
|
+
node_count = 0
|
34
|
+
|
35
|
+
nodes.each do |node|
|
36
|
+
node_count += 1 unless node.text_value.strip.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
node_count
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module NodeProcessing
|
2
|
+
|
3
|
+
|
4
|
+
def flatten(node, leaves = [])
|
5
|
+
# This finds all of the leaves, where a leaf is defined as an
|
6
|
+
# element with no sub elements. This also treats combindable nodes
|
7
|
+
# and text nodes as leaves because we want to keep them intact for
|
8
|
+
# extraction and combination
|
9
|
+
if( !node.respond_to?( :elements) ||
|
10
|
+
node.elements.nil? ||
|
11
|
+
node.elements.empty? ||
|
12
|
+
(node.is_a?( TextNode ) && !node.has_variables?)||
|
13
|
+
node.is_a?( HerbStringVariable ) ||
|
14
|
+
( node.is_a?(NonTextNode) && node.can_be_combined? ) )
|
15
|
+
leaves << node if( !node.text_value.empty? )
|
16
|
+
leaves
|
17
|
+
else
|
18
|
+
node.elements.each do |sub_node|
|
19
|
+
flatten( sub_node, leaves )
|
20
|
+
end
|
21
|
+
leaves
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module NonTextNode
|
2
|
+
include BaseNode
|
3
|
+
def can_be_combined?
|
4
|
+
false
|
5
|
+
end
|
6
|
+
|
7
|
+
def extract_text( text_extractor, node_tree )
|
8
|
+
if( self.white_space? )
|
9
|
+
text_extractor.white_space( self )
|
10
|
+
else
|
11
|
+
text_extractor.add_non_text( self ) unless self.contains_only_whitespace?
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def contains_only_whitespace?
|
17
|
+
self.text_value.strip.length == 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def text?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def top_level?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def white_space?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class RailsTextExtractor < BaseTextExtractor
|
2
|
+
attr_reader :translation_store
|
3
|
+
|
4
|
+
def initialize( translation_store = RailsTranslationStore.new )
|
5
|
+
@key_store = []
|
6
|
+
@translation_store = translation_store
|
7
|
+
@current_text = []
|
8
|
+
@current_nodes = []
|
9
|
+
@current_variables = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# This is called when text extraction has begun
|
13
|
+
def starting_text_extraction
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_html_text
|
17
|
+
@current_text = []
|
18
|
+
@current_nodes = []
|
19
|
+
@current_variables = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def end_html_text
|
23
|
+
whitespace, @current_text = strip_ending_whitespace_nodes( @current_text )
|
24
|
+
|
25
|
+
unless( @current_text.empty? && @current_variables.empty? )
|
26
|
+
total_text = @current_text.inject("") { |all_text, node| all_text + node.text_value }
|
27
|
+
@current_nodes << HerbErbTextCallNode.new( [total_text], @key_store, "t '.", "'", @current_variables )
|
28
|
+
@translation_store.add_translation( @current_nodes.last.key_value, @current_nodes.last.original_text )
|
29
|
+
end
|
30
|
+
|
31
|
+
# Just reset everything here to be cautious
|
32
|
+
to_return = @current_nodes
|
33
|
+
@current_nodes = []
|
34
|
+
@current_text = []
|
35
|
+
|
36
|
+
return to_return + whitespace
|
37
|
+
end
|
38
|
+
|
39
|
+
def white_space( node )
|
40
|
+
if( @current_text.empty? )
|
41
|
+
@current_nodes << node
|
42
|
+
else
|
43
|
+
@current_text << node
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# This is used to add a new variable into the current html element
|
48
|
+
def add_variable( variable_name, variable_value)
|
49
|
+
variable_node = RailsTextVariableNode.new( variable_name, variable_value )
|
50
|
+
@current_text << variable_node
|
51
|
+
@current_variables << variable_node
|
52
|
+
end
|
53
|
+
|
54
|
+
# This is called to just produce a translated text node without erb
|
55
|
+
# braces around it.
|
56
|
+
def translate_text( text_to_translate )
|
57
|
+
call_node = HerbErbTextCallNode.new( [text_to_translate], @key_store, "t '.", "'", [], false )
|
58
|
+
@translation_store.add_translation( call_node.key_value, call_node.original_text )
|
59
|
+
call_node
|
60
|
+
end
|
61
|
+
|
62
|
+
# This takes in a text node and returns one or more nodes that will
|
63
|
+
# then be output. The nodes that are output should implement
|
64
|
+
# node_name and text_value
|
65
|
+
def add_html_text( text_node )
|
66
|
+
@current_text << text_node
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def add_non_text( non_text_node )
|
71
|
+
@current_text << non_text_node
|
72
|
+
end
|
73
|
+
|
74
|
+
# This is called when text extraction has finished
|
75
|
+
def completed_text_extraction
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def strip_ending_whitespace_nodes( node_list )
|
81
|
+
whitespace = []
|
82
|
+
while( !node_list.empty? && node_list.last.white_space? )
|
83
|
+
whitespace << node_list.pop
|
84
|
+
end
|
85
|
+
return whitespace, node_list
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class RailsTranslationStore < BaseTranslationStore
|
2
|
+
|
3
|
+
def initialize( language = "en" )
|
4
|
+
super()
|
5
|
+
@language = language
|
6
|
+
end
|
7
|
+
|
8
|
+
# This goes out of it's way to keep things in exactly the correct
|
9
|
+
# order. This makes it easier for translators when dealing with
|
10
|
+
# files. This is the reason to not just throw things into a hash
|
11
|
+
# and serialize the hash
|
12
|
+
def serialize
|
13
|
+
to_return = "#{@language}:\n"
|
14
|
+
last_context = nil
|
15
|
+
self.each do |context, key, value|
|
16
|
+
whitespace_depth = 2
|
17
|
+
context_array = context.split('/')
|
18
|
+
context_array.each do |split_context|
|
19
|
+
to_return << "#{build_whitespace( whitespace_depth )}#{split_context}:\n"
|
20
|
+
whitespace_depth += 2
|
21
|
+
end unless context == last_context
|
22
|
+
to_return << "#{build_whitespace( 2 + 2 * context_array.length )}#{key}: \"#{escape(value)}\"\n"
|
23
|
+
last_context = context
|
24
|
+
end
|
25
|
+
|
26
|
+
to_return.chomp
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_whitespace( amount )
|
32
|
+
" " * amount
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
|
38
|
+
def escape( key )
|
39
|
+
key.gsub( /"/, '\"' ).gsub( "\n", "\\n" )
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# This is only meant to be a base class, please extend and implement
|
2
|
+
# the base methods if you want to do anything interesting
|
3
|
+
class TextExtractor
|
4
|
+
|
5
|
+
# This is called when text extraction has begun
|
6
|
+
def starting_text_extraction
|
7
|
+
raise "Please implement me (start_text_extraction)"
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_html_text( text_node )
|
11
|
+
raise "Please implement me (add_html_text)"
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_variable( variable_name, variable_value )
|
15
|
+
raise "Please implement me (add_variable)"
|
16
|
+
end
|
17
|
+
|
18
|
+
def translate_text( text_node_to_translate )
|
19
|
+
raise "Please impelement me (translate_text)"
|
20
|
+
end
|
21
|
+
|
22
|
+
def end_html_text
|
23
|
+
raise "Please implement me (end_html_text)"
|
24
|
+
end
|
25
|
+
|
26
|
+
def start_html_text
|
27
|
+
raise "Please implement me (start_html_text)"
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module TextNode
|
2
|
+
include BaseNode
|
3
|
+
include NodeProcessing
|
4
|
+
|
5
|
+
def extract_text( text_extractor, node_tree )
|
6
|
+
if( self.strip_whitespace? && self.has_leading_or_trailing_whitespace? )
|
7
|
+
self.remove_leading_and_trailing_whitespace.each do |node|
|
8
|
+
node.extract_text( text_extractor, node_tree )
|
9
|
+
end
|
10
|
+
else
|
11
|
+
text_extractor.add_html_text( self )
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_variables?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def html?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def strip_whitespace?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def text?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def top_level?
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
def white_space?
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def amount_of_ending_whitespace
|
41
|
+
self.text_value.length - self.text_value.rstrip.length
|
42
|
+
end
|
43
|
+
|
44
|
+
def amount_of_starting_whitespace
|
45
|
+
self.text_value.length - self.text_value.lstrip.length
|
46
|
+
end
|
47
|
+
|
48
|
+
def has_leading_or_trailing_whitespace?
|
49
|
+
amount_of_ending_whitespace > 0 || amount_of_starting_whitespace > 0
|
50
|
+
end
|
51
|
+
|
52
|
+
# This probably won't work, because I want it to retain all of the
|
53
|
+
# nodes. Maybe we should run this after the text extraction
|
54
|
+
# happens? Nope, that won't work....
|
55
|
+
def remove_leading_and_trailing_whitespace
|
56
|
+
to_return = []
|
57
|
+
if( self.text_value.strip.empty? ) # is this only whitespace
|
58
|
+
to_return << HerbWhiteSpaceTextNode.new( self.text_value )
|
59
|
+
else # otherwise it has some text
|
60
|
+
if( amount_of_starting_whitespace > 0 )
|
61
|
+
to_return << HerbWhiteSpaceTextNode.new( self.text_value[0..(amount_of_starting_whitespace - 1 )] )
|
62
|
+
end
|
63
|
+
to_return << HerbTextNode.new( self.text_value.strip )
|
64
|
+
if( amount_of_ending_whitespace > 0 )
|
65
|
+
to_return << HerbWhiteSpaceTextNode.new( self.text_value[(self.text_value.length - amount_of_ending_whitespace)..self.text_value.length ])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
to_return
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
end
|
data/lib/herbgobbler.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
class HerbCombinedNode
|
2
|
+
|
3
|
+
def initialize( node_a, node_b )
|
4
|
+
@combined_nodes = [node_a, node_b]
|
5
|
+
end
|
6
|
+
|
7
|
+
def node_name
|
8
|
+
"herb_combined_nodes"
|
9
|
+
end
|
10
|
+
|
11
|
+
def can_be_combined?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def should_be_unrolled?
|
16
|
+
@combined_nodes.last.white_space?
|
17
|
+
end
|
18
|
+
|
19
|
+
def html?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def text?
|
24
|
+
@combined_nodes.first.text? || @combined_nodes.last.text?
|
25
|
+
end
|
26
|
+
|
27
|
+
def text_value
|
28
|
+
to_return = "";
|
29
|
+
@combined_nodes.each do |node|
|
30
|
+
to_return << node.text_value
|
31
|
+
end
|
32
|
+
to_return
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
self.text_value
|
37
|
+
end
|
38
|
+
|
39
|
+
def whitespace?
|
40
|
+
@combined_nodes.first.whitespace? || @combined_nodes.last.whitespace?
|
41
|
+
end
|
42
|
+
|
43
|
+
def unroll
|
44
|
+
@combined_nodes
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class HerbErbTextCallNode
|
2
|
+
|
3
|
+
def initialize( text_values = [], key_store = [], prepend = nil, postpend = nil, variables = [], include_markup = true )
|
4
|
+
@text_values = text_values
|
5
|
+
@prepend = prepend
|
6
|
+
@postpend = postpend
|
7
|
+
@key_store = key_store
|
8
|
+
@key_value = nil
|
9
|
+
@include_markup = include_markup
|
10
|
+
@variables = variables
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_text( text )
|
14
|
+
@text_values << text
|
15
|
+
end
|
16
|
+
|
17
|
+
def can_be_combined?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def key_value
|
22
|
+
if( @key_value.nil? )
|
23
|
+
@key_value = I18nKey.new( original_text, @key_store ).key_value
|
24
|
+
end
|
25
|
+
|
26
|
+
@key_value
|
27
|
+
end
|
28
|
+
|
29
|
+
def node_name
|
30
|
+
"herb_erb_text_call_node"
|
31
|
+
end
|
32
|
+
|
33
|
+
def original_text
|
34
|
+
@text_values.join( '' )
|
35
|
+
end
|
36
|
+
|
37
|
+
def text_value
|
38
|
+
string_to_return = "#{@prepend}#{key_value}#{@postpend}"
|
39
|
+
string_to_return += ", #{@variables.join( ', ' )}" unless @variables.empty?
|
40
|
+
if( @include_markup )
|
41
|
+
"<%= #{string_to_return} %>"
|
42
|
+
else
|
43
|
+
string_to_return
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
text_value
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class HerbNodeRetainingNode
|
2
|
+
|
3
|
+
def initialize( first_node = nil )
|
4
|
+
@sub_nodes = []
|
5
|
+
@sub_nodes << first_node unless first_node.nil?
|
6
|
+
end
|
7
|
+
|
8
|
+
def nodes
|
9
|
+
to_return = []
|
10
|
+
@sub_nodes.each do|node|
|
11
|
+
if( node.is_a?( HerbNodeRetainingNode ) )
|
12
|
+
to_return += node.nodes
|
13
|
+
else
|
14
|
+
to_return << node
|
15
|
+
end
|
16
|
+
end
|
17
|
+
to_return
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(node)
|
21
|
+
if( node.is_a?(Array) )
|
22
|
+
@sub_nodes+= node
|
23
|
+
else
|
24
|
+
@sub_nodes << node
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def text_value
|
30
|
+
to_return = ""
|
31
|
+
@sub_nodes.each do |node|
|
32
|
+
to_return += node.text_value
|
33
|
+
end
|
34
|
+
to_return
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class HerbNodeRetainingNonTextNode < HerbNodeRetainingNode
|
2
|
+
include NonTextNode
|
3
|
+
|
4
|
+
def initialize( first_node = nil )
|
5
|
+
super( first_node )
|
6
|
+
@can_be_combined = false
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(node)
|
10
|
+
super(node)
|
11
|
+
@can_be_combined ||= ( node.respond_to?( :can_be_combined? ) && node.can_be_combined? )
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_be_combined?
|
15
|
+
@can_be_combined
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.create_from_nodes( nodes )
|
19
|
+
to_return = HerbNodeRetainingNonTextNode.new
|
20
|
+
nodes.each do |node|
|
21
|
+
to_return << node
|
22
|
+
end
|
23
|
+
to_return
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
end
|