glyph 0.2.0 → 0.3.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.
Files changed (114) hide show
  1. data/AUTHORS.textile +1 -1
  2. data/CHANGELOG.textile +119 -222
  3. data/LICENSE.textile +1 -1
  4. data/README.textile +42 -23
  5. data/Rakefile +1 -3
  6. data/VERSION +1 -1
  7. data/benchmark.rb +72 -0
  8. data/book/config.yml +4 -4
  9. data/book/document.glyph +90 -57
  10. data/book/images/document_generation.png +0 -0
  11. data/book/lib/macros/reference.rb +75 -22
  12. data/book/output/html/glyph.html +3183 -2121
  13. data/book/output/html/images/document_generation.png +0 -0
  14. data/book/output/pdf/glyph.pdf +7370 -4913
  15. data/book/resources/document_generation.txt +34 -0
  16. data/book/snippets.yml +6 -0
  17. data/book/text/changelog.glyph +45 -34
  18. data/book/text/compiling/compiling.glyph +23 -0
  19. data/book/text/compiling/lite_mode.glyph +23 -0
  20. data/book/text/compiling/programmatic_usage.glyph +77 -0
  21. data/book/text/extending/bookmarks_headers.glyph +21 -0
  22. data/book/text/extending/further_reading.glyph +13 -0
  23. data/book/text/extending/internals.glyph +79 -0
  24. data/book/text/extending/interpreting.glyph +51 -0
  25. data/book/text/extending/macro_def.glyph +64 -0
  26. data/book/text/extending/params_attrs.glyph +70 -0
  27. data/book/text/extending/placeholders.glyph +34 -0
  28. data/book/text/extending/validators.glyph +16 -0
  29. data/book/text/getting_started/configuration.glyph +49 -0
  30. data/book/text/getting_started/create_project.glyph +41 -0
  31. data/book/text/getting_started/structure.glyph +55 -0
  32. data/book/text/introduction.glyph +49 -26
  33. data/book/text/license.glyph +1 -1
  34. data/book/text/macros/macros_block.glyph +99 -0
  35. data/book/text/macros/macros_core.glyph +208 -0
  36. data/book/text/macros/macros_filters.glyph +40 -0
  37. data/book/text/macros/macros_inline.glyph +50 -0
  38. data/book/text/macros/macros_structure.glyph +100 -0
  39. data/book/text/ref_commands.glyph +94 -73
  40. data/book/text/ref_config.glyph +34 -42
  41. data/book/text/ref_macros.glyph +1 -373
  42. data/book/text/text_editing/code.glyph +51 -0
  43. data/book/text/text_editing/conditionals.glyph +49 -0
  44. data/book/text/text_editing/evaluation.glyph +13 -0
  45. data/book/text/text_editing/glyph_files.glyph +7 -0
  46. data/book/text/text_editing/images.glyph +29 -0
  47. data/book/text/text_editing/inclusions.glyph +44 -0
  48. data/book/text/text_editing/links.glyph +53 -0
  49. data/book/text/text_editing/macro_intro.glyph +111 -0
  50. data/book/text/text_editing/raw_html.glyph +112 -0
  51. data/book/text/text_editing/sections.glyph +63 -0
  52. data/book/text/text_editing/stylesheets.glyph +36 -0
  53. data/book/text/troubleshooting/errors_command.glyph +39 -0
  54. data/book/text/troubleshooting/errors_generic.glyph +29 -0
  55. data/book/text/troubleshooting/errors_intro.glyph +3 -0
  56. data/book/text/troubleshooting/errors_macro.glyph +98 -0
  57. data/book/text/troubleshooting/errors_parser.glyph +29 -0
  58. data/config.yml +77 -58
  59. data/document.glyph +25 -25
  60. data/glyph.gemspec +57 -22
  61. data/lib/glyph.rb +54 -13
  62. data/lib/glyph/commands.rb +84 -17
  63. data/lib/glyph/config.rb +3 -3
  64. data/lib/glyph/document.rb +14 -8
  65. data/lib/glyph/interpreter.rb +18 -58
  66. data/lib/glyph/macro.rb +160 -55
  67. data/lib/glyph/macro_validators.rb +104 -12
  68. data/lib/glyph/node.rb +24 -0
  69. data/lib/glyph/parser.rb +278 -0
  70. data/lib/glyph/syntax_node.rb +225 -0
  71. data/macros/core.rb +212 -0
  72. data/macros/filters.rb +66 -15
  73. data/macros/html/block.rb +43 -105
  74. data/macros/html/inline.rb +11 -12
  75. data/macros/html/structure.rb +123 -58
  76. data/macros/xml.rb +33 -0
  77. data/spec/files/container.textile +2 -2
  78. data/spec/files/document.glyph +2 -2
  79. data/spec/files/document_with_toc.glyph +3 -3
  80. data/spec/files/included.textile +1 -1
  81. data/spec/files/ligature.jpg +0 -0
  82. data/spec/files/markdown.markdown +2 -1
  83. data/spec/lib/commands_spec.rb +46 -3
  84. data/spec/lib/document_spec.rb +4 -4
  85. data/spec/lib/glyph_spec.rb +17 -46
  86. data/spec/lib/interpreter_spec.rb +6 -25
  87. data/spec/lib/macro_spec.rb +141 -43
  88. data/spec/lib/macro_validators_spec.rb +27 -5
  89. data/spec/lib/node_spec.rb +26 -1
  90. data/spec/lib/parser_spec.rb +246 -0
  91. data/spec/lib/syntax_node_spec.rb +111 -0
  92. data/spec/macros/core_spec.rb +195 -0
  93. data/spec/macros/filters_spec.rb +38 -4
  94. data/spec/macros/macros_spec.rb +20 -176
  95. data/spec/macros/textile_spec.rb +13 -71
  96. data/spec/macros/xml_spec.rb +77 -0
  97. data/spec/spec_helper.rb +50 -10
  98. data/spec/tasks/load_spec.rb +13 -2
  99. data/styles/default.css +18 -6
  100. data/styles/pagination.css +1 -19
  101. data/tasks/generate.rake +2 -2
  102. data/tasks/load.rake +27 -17
  103. data/tasks/project.rake +1 -1
  104. metadata +75 -62
  105. data/book/script/compile.rb +0 -8
  106. data/book/script/prof +0 -1
  107. data/book/script/prof_results.htm +0 -21079
  108. data/book/text/authoring.glyph +0 -548
  109. data/book/text/extending.glyph +0 -224
  110. data/book/text/getting_started.glyph +0 -158
  111. data/book/text/troubleshooting.glyph +0 -179
  112. data/lib/glyph/glyph_language.rb +0 -538
  113. data/lib/glyph/glyph_language.treetop +0 -27
  114. data/macros/common.rb +0 -160
@@ -1,48 +1,140 @@
1
1
  module Glyph
2
2
  class Macro
3
3
 
4
+ # @since 0.2.0
4
5
  module Validators
5
6
 
6
7
  # Validates the macro according to the specified block
7
8
  # @param [String] message the message to display if the validation fails.
8
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
9
+ # @param [Hash] options a hash containing validation options
10
+ # @option options :level the error level (:error, :warning)
11
+ # @return [Boolean] whether the validation passed or not
9
12
  # @example
10
- # validate("Invalid macro value", :level => :error) {@value == 'valid'} # Raises an error in case of failure
11
- # validate("Invalid macro value", :level => :warning) {@value == 'valid'} # Displays a warning in case of failure
13
+ # validate("Invalid macro value", :level => :error) {value == 'valid'} # Raises an error in case of failure
14
+ # validate("Invalid macro value", :level => :warning) {value == 'valid'} # Displays a warning in case of failure
12
15
  def validate(message, options={:level => :error}, &block)
13
- unless instance_eval(&block) then
16
+ result = instance_eval(&block)
17
+ unless result then
14
18
  send("macro_#{options[:level]}".to_sym, message)
15
19
  end
20
+ result
21
+ end
22
+
23
+ # Ensures that the macro element attributes is a valid XML element name.
24
+ # @param [Hash] options a hash containing validation options (for now the only option is :level)
25
+ # @return [Boolean] whether the validation passed or not
26
+ # @since 0.3.0
27
+ def valid_xml_element(options={:level => :error})
28
+ validate("Invalid XML element '#{@node[:element]}'", options) { @node[:element].to_s.match(/^([^[:punct:]0-9<>]|_)[^<>"']*/) }
29
+ end
30
+
31
+ # Ensures that a macro attribute name is a valid XML attribute name.
32
+ # @param [String, Symbol] name the attribute name to validate
33
+ # @param [Hash] options a hash containing validation options
34
+ # @option options :level the error level (:error, :warning)
35
+ # @return [Boolean] whether the validation passed or not
36
+ # @since 0.3.0
37
+ def valid_xml_attribute(name, options={:level => :warning})
38
+ validate("Invalid XML attribute '#{name}'", options) { name.to_s.match(/^([^[:punct:]0-9<>]|_)[^<>"']*/) }
16
39
  end
17
40
 
18
41
  # Ensures that the macro receives up to _n_ parameters.
19
42
  # @param [Integer] n the maximum number of parameters allowed for the macro.
20
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
43
+ # @param [Hash] options a hash containing validation options
44
+ # @option options :level the error level (:error, :warning)
45
+ # @return [Boolean] whether the validation passed or not
21
46
  def max_parameters(n, options={:level=>:error})
22
- validate("Macro '#{@name}' takes up to #{n} parameter(s) (#{params.length} given)", options) { params.length <= n }
47
+ validate("Macro '#{@name}' takes up to #{n} parameter(s) (#{@node.params.length} given)", options) do
48
+ if n == 0 then
49
+ no_parameters options
50
+ else
51
+ @node.params.length <= n
52
+ end
53
+ end
23
54
  end
24
55
 
25
56
  # Ensures that the macro receives at least _n_ parameters.
26
57
  # @param [Integer] n the minimum number of parameters allowed for the macro.
27
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
58
+ # @param [Hash] options a hash containing validation options
59
+ # @option options :level the error level (:error, :warning)
60
+ # @return [Boolean] whether the validation passed or not
28
61
  def min_parameters(n, options={:level=>:error})
29
- validate("Macro '#{@name}' takes at least #{n} parameter(s) (#{params.length} given)", options) { params.length >= n }
62
+ validate("Macro '#{@name}' takes at least #{n} parameter(s) (#{@node.params.length} given)", options) { @node.params.length >= n }
30
63
  end
31
64
 
32
65
  # Ensures that the macro receives exactly _n_ parameters.
33
66
  # @param [Integer] n the number of parameters allowed for the macro.
34
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
67
+ # @param [Hash] options a hash containing validation options
68
+ # @option options :level the error level (:error, :warning)
69
+ # @return [Boolean] whether the validation passed or not
35
70
  def exact_parameters(n, options={:level=>:error})
36
- validate("Macro '#{@name}' takes exactly #{n} parameter(s) (#{params.length} given)", options) { params.length == n }
71
+ validate("Macro '#{@name}' takes exactly #{n} parameter(s) (#{@node.params.length} given)", options) do
72
+ if n == 0 then
73
+ no_parameters options
74
+ else
75
+ @node.params.length == n
76
+ end
77
+ end
37
78
  end
38
79
 
39
80
  # Ensures that the macro receives no parameters.
40
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
81
+ # @param [Hash] options a hash containing validation options
82
+ # @option options :level the error level (:error, :warning)
83
+ # @return [Boolean] whether the validation passed or not
41
84
  def no_parameters(options={:level=>:error})
42
- validate("Macro '#{@name}' takes no parameters (#{params.length} given)", options) { params.length == 0 }
85
+ validate("Macro '#{@name}' takes no parameters (#{@node.params.length} given)", options) do
86
+ case @node.params.length
87
+ when 0 then
88
+ true
89
+ when 1 then
90
+ result = true
91
+ @node.param(0).children.each do |p|
92
+ result = p.is_a?(Glyph::TextNode) && p[:value].blank?
93
+ break unless result
94
+ end
95
+ result
96
+ else
97
+ false
98
+ end
99
+ end
100
+ end
101
+
102
+ # Raises a macro error if Glyph is running in safe mode.
103
+ # @raise [Glyph::MacroError] the macro cannot be used allowed in safe mode
104
+ # @since 0.3.0
105
+ def safety_check
106
+ macro_error "Macro '#@name' cannot be used in safe mode" if Glyph.safe?
43
107
  end
44
108
 
109
+ # Ensure that no mutual inclusion occurs within the specified parameter or attribute
110
+ # @param [Fixnum, Symbol] the parameter index or attribute name to check
111
+ # @raise [Glyph::MacroError] mutual inclusion was detected
112
+ # @since 0.3.0
113
+ def no_mutual_inclusion_in(arg)
114
+ check_type = arg.is_a?(Symbol) ? :attribute : :parameter
115
+ check_value = nil
116
+ found = @node.find_parent do |n|
117
+ if n.is_a?(Glyph::MacroNode) && Glyph::MACROS[n[:name]] == Glyph::MACROS[@name] then
118
+ case check_type
119
+ when :attribute then
120
+ check_value = n.children.select do |node|
121
+ node.is_a?(Glyph::AttributeNode) && node[:name] == arg
122
+ end[0][:value] rescue nil
123
+ check_value == attr(arg)
124
+ when :parameter then
125
+ check_value = n.children.select do |node|
126
+ node.is_a?(Glyph::ParameterNode) && node[:name] == :"#{arg}"
127
+ end[0][:value] rescue nil
128
+ check_value == param(arg)
129
+ end
130
+ end
131
+ end
132
+ if found then
133
+ macro_error "Mutual Inclusion in #{check_type}(#{arg}): '#{check_value}'", Glyph::MutualInclusionError
134
+ end
135
+ end
45
136
  end
46
137
 
47
138
  end
139
+
48
140
  end
data/lib/glyph/node.rb CHANGED
@@ -135,4 +135,28 @@ class Node < Hash
135
135
  ascend(parent) {|e| return e unless e.parent }
136
136
  end
137
137
 
138
+ # Converts self to a hash
139
+ # @return [Hash] the converted hash
140
+ # @since 0.3.0
141
+ def to_hash
142
+ {}.merge(self)
143
+ end
144
+
145
+ # @return [String] a textual representation of self
146
+ # @since 0.3.0
147
+ def inspect
148
+ string = ""
149
+ descend do |e, level|
150
+ string << " "*level+e.to_hash.inspect+"\n"
151
+ end
152
+ string.chomp
153
+ end
154
+
155
+ # @return (Boolean) true if the nodes are equal
156
+ # @since 0.3.0
157
+ def ==(node)
158
+ return false unless node.is_a? Node
159
+ self.to_hash == node.to_hash && self.children == node.children
160
+ end
161
+
138
162
  end
@@ -0,0 +1,278 @@
1
+ # encoding: utf-8
2
+ require 'strscan'
3
+
4
+ module Glyph
5
+
6
+ # The Glyph::Parser class can parse a string of text containing Glyph macros and produce the
7
+ # corresponding syntax tree.
8
+ # @since 0.3.0
9
+ class Parser
10
+
11
+ # Initializes the parser.
12
+ # @param [String] text the text to parse
13
+ # @param [String] source_name the name of the source file (stored in the root node)
14
+ # @since 0.3.0
15
+ def initialize(text, source_name="--")
16
+ @source_name = source_name || "--"
17
+ @input = StringScanner.new text
18
+ @output = create_node DocumentNode, :name => @source_name.to_sym
19
+ @current_macro = nil
20
+ @current_attribute = nil
21
+ end
22
+
23
+ # Parses the string of text provided during initialization
24
+ # @return [Glyph::SyntaxNode] the Abstract Syntax Tree corresponding to the string
25
+ # @since 0.3.0
26
+ def parse
27
+ count = 0
28
+ while result = parse_contents(@output) do
29
+ @output << result
30
+ count +=1
31
+ end
32
+ if @input.pos < @input.string.length then
33
+ current_char = @input.string[@input.pos].chr
34
+ illegal_delimiter = current_char.match(/\]|\[/) rescue nil
35
+ error "Macro delimiter '#{current_char}' not escaped" if illegal_delimiter
36
+ end
37
+ @output
38
+ end
39
+
40
+ protected
41
+
42
+ def parse_contents(current)
43
+ escape_sequence(current) ||
44
+ parameter_delimiter(current) ||
45
+ escaping_attribute(current) ||
46
+ escaping_macro(current) ||
47
+ attribute(current) ||
48
+ macro(current) ||
49
+ text(current)
50
+ end
51
+
52
+ def parse_escaped_contents(current)
53
+ escape_sequence(current) || parameter_delimiter(current) || escaped_text(current)
54
+ end
55
+
56
+ def escaping_macro(current)
57
+ if @input.scan(/[^\[\]\|\\\s]+\[\=/) then
58
+ name = @input.matched
59
+ name.chop!
60
+ name.chop!
61
+ error "#{name}[...] - A macro cannot start with '@' or a digit." if name.match(/^[0-1@]/)
62
+ node = create_node(MacroNode, {
63
+ :name => name.to_sym,
64
+ :escape => true,
65
+ :attributes => [],
66
+ :parameters => []
67
+ })
68
+ while contents = parse_escaped_contents(node) do
69
+ node << contents unless contents.is_a?(AttributeNode)
70
+ end
71
+ @input.scan(/\=\]/) or error "Escaping macro '#{name}' not closed"
72
+ organize_children_for node
73
+ node
74
+ else
75
+ nil
76
+ end
77
+ end
78
+
79
+ def escaping_attribute(current)
80
+ if @input.scan(/@[^\[\]\|\\\s]+\[\=/) then
81
+ error "Attributes cannot be nested" if @current_attribute
82
+ name = @input.matched[1..@input.matched.length-3]
83
+ node = create_node(AttributeNode, {
84
+ :escape => true,
85
+ :name => name.to_sym
86
+ })
87
+ while contents = parse_escaped_contents(node) do
88
+ node << contents
89
+ end
90
+ current[:attributes] << node
91
+ @input.scan(/\=\]/) or error "Attribute @#{name} not closed"
92
+ node
93
+ else
94
+ nil
95
+ end
96
+ end
97
+
98
+ def macro(current)
99
+ if @input.scan(/[^\[\]\|\\\s]+\[/) then
100
+ name = @input.matched
101
+ name.chop!
102
+ error "#{name}[...] - A macro cannot start with '@' or a digit." if name.match(/^[0-1@]/)
103
+ node = create_node(MacroNode, {
104
+ :escape => false,
105
+ :name => name.to_sym,
106
+ :attributes => [],
107
+ :parameters => []
108
+ })
109
+ while contents = parse_contents(node) do
110
+ node << contents unless contents.is_a?(AttributeNode)
111
+ end
112
+ @input.scan(/\]/) or error "Macro '#{name}' not closed"
113
+ organize_children_for node
114
+ node
115
+ else
116
+ nil
117
+ end
118
+ end
119
+
120
+ def attribute(current)
121
+ if @input.scan(/@[^\[\]\|\\\s]+\[/) then
122
+ error "Attributes cannot be nested" if current.is_a?(AttributeNode)
123
+ name = @input.matched[1..@input.matched.length-2]
124
+ node = create_node(AttributeNode, {
125
+ :escape => false,
126
+ :name => name.to_sym
127
+ })
128
+ while contents = parse_contents(node) do
129
+ node << contents
130
+ end
131
+ current[:attributes] << node
132
+ @input.scan(/\]/) or error "Attribute @#{name} not closed"
133
+ node
134
+ else
135
+ nil
136
+ end
137
+ end
138
+
139
+ def text(current)
140
+ start_p = @input.pos
141
+ res = @input.scan_until /(\\.)|(\A(\]|\|)|[^\\](\]|\|)|[^\[\]\|\\\s]+\[|\Z)/
142
+ offset = @input.matched.match(/^[^\\](\]|\|)$/) ? 1 : @input.matched.length
143
+ @input.pos = @input.pos - offset rescue @input.pos
144
+ return nil if @input.pos == start_p
145
+ match = @input.string[start_p..@input.pos-1]
146
+ illegal_macro_delimiter? start_p, match
147
+ if match.length > 0 then
148
+ create_node TextNode, :value => match
149
+ else
150
+ nil
151
+ end
152
+ end
153
+
154
+ def escaped_text(current)
155
+ start_p = @input.pos
156
+ res = @input.scan_until /(\\.)|(\A(\=\]|\|)|[^\\](\=\]|\|)|\Z)/
157
+ case
158
+ when @input.matched.match(/^[^\\]\=\]$/) then
159
+ offset = 2
160
+ when @input.matched.match(/^[^\\]\|$/) then
161
+ offset = 1
162
+ else
163
+ offset = @input.matched.length
164
+ end
165
+ @input.pos = @input.pos - offset rescue @input.pos
166
+ return nil if @input.pos == start_p
167
+ match = @input.string[start_p..@input.pos-1]
168
+ illegal_nesting = match.match(/([^\[\]\|\\\s]+)\[\=/)[1] rescue nil
169
+ if illegal_nesting then
170
+ error "Cannot nest escaping macro '#{illegal_nesting}' within escaping macro '#{current[:name]}'"
171
+ end
172
+ if match.length > 0 then
173
+ create_node TextNode, :value => match, :escaped => true
174
+ else
175
+ nil
176
+ end
177
+ end
178
+
179
+ def parameter_delimiter(current)
180
+ if @input.scan(/\|/) then
181
+ # Parameters are not allowed outside macros or inside attributes
182
+ if current.is_a?(DocumentNode) || current.is_a?(AttributeNode) then
183
+ @input.pos = @input.pos-1
184
+ error "Parameter delimiter '|' not allowed here"
185
+ end
186
+ create_node SyntaxNode, :parameter => true
187
+ else
188
+ nil
189
+ end
190
+ end
191
+
192
+ def escape_sequence(current)
193
+ if @input.scan(/\\./) then
194
+ create_node EscapeNode, :value => @input.matched, :escaped => true
195
+ end
196
+ end
197
+
198
+ private
199
+
200
+ def aggregate_parameters_for(node)
201
+ indices = []
202
+ count = 0
203
+ node.children.each do |n|
204
+ indices << count if n[:parameter]
205
+ count += 1
206
+ end
207
+ # No parameter found
208
+ if indices == [] then
209
+ node[:parameters][0] = create_node ParameterNode, :name => :"0"
210
+ node.children.each do |c|
211
+ node[:parameters][0] << c
212
+ end
213
+ else
214
+ # Parameters found
215
+ current_index = 0
216
+ total_parameters = 0
217
+ save_parameter = lambda do |max_index|
218
+ parameter = create_node ParameterNode, :name => "#{total_parameters}".to_sym
219
+ total_parameters +=1
220
+ current_index.upto(max_index) do |index|
221
+ parameter << (node & index)
222
+ end
223
+ node[:parameters] << parameter
224
+ end
225
+ indices.each do |i|
226
+ save_parameter.call(i-1)
227
+ current_index = i+1
228
+ end
229
+ save_parameter.call(node.children.length-1)
230
+ end
231
+ node[:parameters]
232
+ end
233
+
234
+ def organize_children_for(node)
235
+ aggregate_parameters_for node
236
+ node.children.clear
237
+ node[:parameters].each do |p|
238
+ node << p
239
+ end
240
+ empty_parameter =
241
+ node.children.length == 1 &&
242
+ ((node&0).children.length == 0 ||
243
+ (node&0).children.length == 0 &&
244
+ (node&0&0).is_a?(TextNode) &&
245
+ (node&0&0)[:value].blank?)
246
+ node.children.clear if empty_parameter
247
+ node.delete(:parameters)
248
+ node[:attributes].each do |a|
249
+ node << a
250
+ end
251
+ node.delete(:attributes)
252
+ end
253
+
254
+ def illegal_macro_delimiter?(start_p, string)
255
+ string.match(/\A(\[|\])|[^\\](\[|\])/)
256
+ illegal_delimiter = $1 || $2
257
+ if illegal_delimiter then
258
+ @input.pos = start_p + string.index(illegal_delimiter)
259
+ error "Macro delimiter '#{illegal_delimiter}' not escaped"
260
+ end
261
+ end
262
+
263
+ def error(msg)
264
+ lines = @input.string[0..@input.pos].split(/\n/)
265
+ line = lines.length
266
+ column = lines.last.length
267
+ raise Glyph::SyntaxError.new("#{@source_name} [#{line}, #{column}] "+msg)
268
+ end
269
+
270
+ def create_node(klass, hash={})
271
+ klass.new.from hash
272
+ end
273
+
274
+ end
275
+
276
+ end
277
+
278
+