ruql 0.0.9 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4efcebefa4b89ca759cce170acace5355feaeefd
4
- data.tar.gz: d7ab50a7a43b935ec9fbba0919f039b00c843b3d
3
+ metadata.gz: 8a6637987324f5db32e7387b7183ea4591957817
4
+ data.tar.gz: 05dc9bbffafb43de71688bbbd1a0b70302d51665
5
5
  SHA512:
6
- metadata.gz: 149a8f650812e0d3e6be26670448d4d56388c0847cbfc3a901504ee91af388dc349e47fb7aef116da0634a64d11ca2803674f400814f223c8ed07d5f85cdd164
7
- data.tar.gz: efb47e061b140a3bca3f1cc8001631e2bf08b5c342c5225a663455b62922e6e05e41f74ca2e388fcc6f37ea25de3561c383851206647ea9d68958c0db7dfa40c
6
+ metadata.gz: bd17352a49b61a9b0844cb0d043093c3ba8fe33ba302ac83aac60cb602a4f534074f7c02c4dd0f0772d301232dd360c57d997ddd6fd67ac171172983b0e8c93b
7
+ data.tar.gz: 2f34ee1cd6b63f9f81ccd6bafecc7864143d4512d127d1a5740e035f73d9cf5c83abd68bd0eda6b232837bb1a68e60223977ca38920c9a8926b1d167779fd25d
data/bin/ruql CHANGED
@@ -15,7 +15,6 @@ renderer choices are:
15
15
  HtmlForm - HTML forms
16
16
  EdXml - XML for OpenEdX platform in-course questions
17
17
  AutoQCM - LaTeX for use with AutoQCM (http://home.gna.org/auto-qcm)
18
- Coursera - [obsolete] Coursera HTML/XML format for online auto-grading
19
18
  JSON - [partially implemented] JSON format
20
19
  Qualtrics -[partially implemented] Qualtrics survey (txt) format
21
20
 
@@ -39,7 +38,7 @@ The EdXML renderer supports these options:
39
38
  some systems.
40
39
 
41
40
  -y <file.yml>, --yaml=<file.yml>
42
- Render the questions with a specific yaml file.
41
+ Render open-assessment questions using info in given Yaml file.
43
42
 
44
43
  The HTML5 and HTML Forms renderers supports these options:
45
44
  -o <type>, --list-type=<type>
@@ -47,14 +46,13 @@ The HTML5 and HTML Forms renderers supports these options:
47
46
  Sets the values of the 'start' and 'type' attributes for the HTML <ol>
48
47
  element. Type should be one of 1,a,A,i,I. Start should always be
49
48
  a number indicating the ordinal value (starting from 1) of first item.
50
- -c <href>, --css=<href>
51
- embed <href> for stylesheet into generated HTML5
52
49
  -j <src>, --js=<src>
53
50
  embed <src> for JavaScript
54
51
  -t <file.html.erb>, --template=<file.html.erb>
55
52
  Use file.html.erb as HTML template rather than generating our own file.
56
53
  file.html.erb should have <%= yield %> where questions should go.
57
- An example is in the templates/ directory of RuQL.
54
+ The default template in the templates/ directory of RuQL will be used
55
+ if this option is not given.
58
56
  The following local variables will be replaced with their values in
59
57
  the template:
60
58
  <%= quiz.title %> - the quiz title
@@ -99,11 +97,9 @@ def main
99
97
  ['-l', '--log', Getopt::REQUIRED],
100
98
  ['-y', '--yaml', Getopt::REQUIRED]
101
99
  )
102
- Quiz.set_yaml_file opts.delete("y") || opts.delete("yaml")
103
100
  Quiz.instance_eval "#{IO.read(filename)}"
104
101
  Quiz.quizzes.each { |quiz| puts quiz.render_with(renderer, opts) }
105
102
  end
106
103
 
107
104
  usage if ARGV.length < 2
108
105
  main
109
-
data/lib/ruql/answer.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  class Answer
2
2
  include Comparable
3
- attr_accessor :answer_text, :explanation
3
+ attr_accessor :answer_text, :explanation, :correct
4
4
  attr_reader :correct
5
5
  attr_reader :builder
6
6
  attr_reader :question
@@ -8,9 +8,21 @@ class Answer
8
8
  def <=>(other) ; self.answer_text <=> other.answer_text ; end
9
9
  def correct? ; !!correct ; end
10
10
  def has_explanation? ; !!explanation ; end
11
- def initialize(answer_text, correct, explanation=nil)
11
+ def initialize(answer_text = '', correct = false, explanation=nil)
12
12
  @answer_text = answer_text
13
13
  @correct = !!correct # ensure boolean
14
14
  @explanation = explanation
15
15
  end
16
+
17
+ def to_JSON
18
+ Hash[instance_variables.collect { |var| [var.to_s.delete('@'), instance_variable_get(var)] }]
19
+ end
20
+
21
+ def self.from_JSON(hash)
22
+ answer = Answer.new
23
+ hash.each do |key, value|
24
+ answer.send((key + '=').to_sym, value)
25
+ end
26
+ answer
27
+ end
16
28
  end
@@ -1,10 +1,10 @@
1
1
  class MultipleChoice < Question
2
-
3
2
  attr_accessor :multiple
4
3
 
5
4
  def initialize(text='', opts={})
6
5
  super
7
6
  self.question_text = text
7
+
8
8
  self.multiple = !!opts[:multiple]
9
9
  self.randomize = !!opts[:randomize]
10
10
  end
@@ -115,7 +115,7 @@ class OpenAssessment
115
115
  # Adds fields for a simple_open_assessment question
116
116
  def add_simple_question
117
117
  criterion = Criterion.new
118
- criterion.name("How'd you do?")
118
+ criterion.name("How'd you do?") # "
119
119
  criterion.label("Scoring Rubric")
120
120
 
121
121
  raise "Must have answer for question" if @question_answer.nil?
data/lib/ruql/question.rb CHANGED
@@ -1,24 +1,40 @@
1
1
  class Question
2
- attr_accessor :question_text, :answers, :randomize, :points, :name, :question_tags, :question_comment
3
-
2
+ attr_accessor :question_text,
3
+ :answers,
4
+ :question_image,
5
+ :randomize,
6
+ :points,
7
+ :name,
8
+ :question_tags,
9
+ :question_uid,
10
+ :question_comment,
11
+ :raw
12
+
4
13
  def initialize(*args)
5
14
  options = if args[-1].kind_of?(Hash) then args[-1] else {} end
6
15
  @answers = options[:answers] || []
7
16
  @points = [options[:points].to_i, 1].max
8
17
  @raw = options[:raw]
9
18
  @name = options[:name]
19
+ @question_image = options[:image]
10
20
  @question_tags = []
21
+ @question_uid = (options.delete(:uid) || SecureRandom.uuid).to_s
11
22
  @question_comment = ''
12
23
  end
13
-
14
24
  def raw? ; !!@raw ; end
15
-
25
+
26
+ def uid(u) ; @question_uid = u ; end
27
+
16
28
  def text(s) ; @question_text = s ; end
17
29
 
18
30
  def explanation(text)
19
31
  @answers.each { |answer| answer.explanation ||= text }
20
32
  end
21
33
 
34
+ def image(url)
35
+ @question_image = url
36
+ end
37
+
22
38
  def answer(text, opts={})
23
39
  @answers << Answer.new(text, correct=true, opts[:explanation])
24
40
  end
@@ -44,4 +60,30 @@ class Question
44
60
 
45
61
  def correct_answers ; @answers.collect(&:correct?) ; end
46
62
 
63
+ def answer_helper(obj)
64
+ if obj.is_a? Array and obj.size and obj[0].is_a? Answer
65
+ return obj.map {|answer| answer.to_JSON}
66
+ end
67
+ obj
68
+ end
69
+
70
+ #creates a JSON hash of the object with its object name. we should convert this to a mixin for answer and question. aaron
71
+ def to_JSON
72
+ h = Hash[instance_variables.collect { |var| [var.to_s.delete('@'), answer_helper(instance_variable_get(var))] }]
73
+ h['question_type'] = self.class.to_s
74
+ return h
75
+ end
76
+
77
+ #factory method to return correct type of question
78
+ def self.from_JSON(hash_str)
79
+ hash = JSON.parse(hash_str)
80
+ #create the appropriate class of the object from the hash's class name
81
+ question = Object.const_get(hash.fetch('question_type')).new()
82
+ hash.reject{|key| key == 'answers' or key == 'question_type'}.each do |key, value|
83
+ question.send((key + '=').to_sym, value)
84
+ end
85
+ question.answers = hash['answers'].map{|answer_hash| Answer.from_JSON(answer_hash)}
86
+ question
87
+ end
88
+
47
89
  end
data/lib/ruql/quiz.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  class Quiz
2
2
  @@quizzes = []
3
- @@yaml_file = nil
4
3
  @quiz_yaml = {}
5
4
  def self.quizzes ; @@quizzes ; end
6
5
  @@default_options =
@@ -27,18 +26,20 @@ class Quiz
27
26
  attr_reader :output
28
27
  attr_reader :seed
29
28
  attr_reader :logger
30
- attr_accessor :title
29
+ attr_accessor :title, :quizzes
31
30
 
32
- def initialize(title, yaml, options={})
31
+ def initialize(title, options={})
33
32
  @output = ''
34
- @questions = options[:questions] || []
33
+ @questions = options.delete(:questions) || []
35
34
  @title = title
36
35
  @options = @@default_options.merge(options)
37
36
  @seed = srand
38
37
  @logger = Logger.new(STDERR)
39
38
  @logger.level = Logger.const_get (options.delete('l') ||
40
39
  options.delete('log') || 'warn').upcase
41
- @quiz_yaml = yaml
40
+ if (yaml = options.delete(:yaml))
41
+ @quiz_yaml = YAML::load(IO.read yaml)
42
+ end
42
43
  end
43
44
 
44
45
  def self.get_renderer(renderer)
@@ -52,10 +53,6 @@ class Quiz
52
53
  @output = @renderer.output
53
54
  end
54
55
 
55
- def self.set_yaml_file(file)
56
- @@yaml_file = file && YAML::load_file(file)
57
- end
58
-
59
56
  def points ; questions.map(&:points).inject { |sum,points| sum + points } ; end
60
57
 
61
58
  def num_questions ; questions.length ; end
@@ -130,7 +127,7 @@ class Quiz
130
127
  end
131
128
 
132
129
  def self.quiz(*args, &block)
133
- quiz = Quiz.new(*args, (@@yaml_file.shift if @@yaml_file))
130
+ quiz = Quiz.new(*args)
134
131
  quiz.instance_eval(&block)
135
132
  @@quizzes << quiz
136
133
  end
@@ -4,7 +4,7 @@ require 'yaml'
4
4
  class EdXmlRenderer
5
5
 
6
6
  attr_reader :output
7
- attr_accessor :file, :yaml_file
7
+ attr_accessor :file
8
8
  def initialize(quiz,options={})
9
9
  @only_question = options.delete('n') || options.delete('name')
10
10
  @output = ''
@@ -5,11 +5,10 @@ class Html5Renderer
5
5
  attr_reader :output
6
6
 
7
7
  def initialize(quiz,options={})
8
- @css = options.delete('c') || options.delete('css')
9
8
  @show_solutions = options.delete('s') || options.delete('solutions')
10
9
  @template = options.delete('t') ||
11
10
  options.delete('template') ||
12
- File.join(Gem.loaded_specs['ruql'].full_gem_path, 'templates/html5.html.erb')
11
+ File.join(File.dirname(__FILE__), '../../../templates/html5.html.erb')
13
12
  @output = ''
14
13
  @list_type = options.delete('o') || options.delete('list-type') || '1'
15
14
  @list_start = options.delete('a') || options.delete('list-start') || '1'
@@ -18,21 +17,9 @@ class Html5Renderer
18
17
  end
19
18
 
20
19
  def render_quiz
21
- if @template
22
- render_with_template do
23
- render_questions
24
- @output
25
- end
26
- else
27
- @h.html do
28
- @h.head do
29
- @h.title @quiz.title
30
- @h.link(:rel => 'stylesheet', :type =>'text/css', :href=>@css) if @css
31
- end
32
- @h.body do
33
- render_questions
34
- end
35
- end
20
+ render_with_template do
21
+ render_questions
22
+ @output
36
23
  end
37
24
  self
38
25
  end
@@ -124,10 +111,17 @@ class Html5Renderer
124
111
  def render_question_text(question,index)
125
112
  html_args = {
126
113
  :id => "question-#{index}",
127
- :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')]
128
- .join(' ')
114
+ :'data-uid' => question.question_uid,
115
+ :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')].join(' ')
129
116
  }
117
+ if question.question_image # add CSS class to both <li> and <img>
118
+ html_args[:class] << 'question-with-image'
119
+ end
130
120
  @h.li html_args do
121
+ # if there's an image, render it first
122
+ if question.question_image
123
+ @h.img :src => question.question_image, :class => 'question-image'
124
+ end
131
125
  @h.div :class => 'text' do
132
126
  qtext = "[#{question.points} point#{'s' if question.points>1}] " <<
133
127
  ('Select ALL that apply: ' if question.multiple).to_s <<
@@ -10,40 +10,6 @@ class JSONRenderer
10
10
  end
11
11
 
12
12
  def render_quiz
13
- @quiz.questions.each do |question|
14
- case question
15
- when MultipleChoice, SelectMultiple, TrueFalse then render_multiple_choice(question)
16
- when FillIn then render_fill_in(question) # not currently supported
17
- else
18
- raise "Unknown question type: #{question}"
19
- end
20
- end
21
- @output = JSON.pretty_generate(@json_array)
13
+ @output = @quiz.questions.map {|question| JSON.pretty_generate(question.to_JSON)}
22
14
  end
23
-
24
- def render_multiple_choice(question)
25
- question_hash = {
26
- "text" => question.question_text,
27
- "answers" => answers_to_json_array(question.answers)
28
- }
29
- @json_array.push(question_hash)
30
- end
31
-
32
- def answers_to_json_array(answers)
33
- answers_array = []
34
- answers.each do |answer|
35
- answer_json = {
36
- "text" => answer.answer_text,
37
- "explanation" => answer.explanation,
38
- "correct" => answer.correct
39
- }
40
- answers_array.push(answer_json)
41
- end
42
- answers_array
43
- end
44
-
45
- def render_fill_in(q)
46
- # fill-in-the-blank questions not currently supported
47
- end
48
-
49
15
  end
@@ -1,5 +1,4 @@
1
1
  class SelectMultiple < MultipleChoice
2
-
3
2
  def initialize(text='', opts={})
4
3
  super
5
4
  self.multiple = true
@@ -1,6 +1,6 @@
1
1
  class TrueFalse < Question
2
2
 
3
- def initialize(text, correct_answer, opts=nil)
3
+ def initialize(text='', correct_answer=false, opts=nil)
4
4
  super
5
5
  opts ||= {}
6
6
  opts[:explanation] ||= ''
data/lib/ruql.rb CHANGED
@@ -6,13 +6,7 @@ require 'date'
6
6
  $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__)))
7
7
 
8
8
  # renderers
9
- require 'ruql/renderers/xml_renderer'
10
- require 'ruql/renderers/html5_renderer'
11
- require 'ruql/renderers/html_form_renderer'
12
- require 'ruql/renderers/edxml_renderer'
13
- require 'ruql/renderers/auto_qcm_renderer'
14
- require 'ruql/renderers/json_renderer'
15
- require 'ruql/renderers/qualtrics_renderer'
9
+ Dir[File.join(File.dirname(__FILE__),'ruql/renderers/*.rb')].each { |file| require file }
16
10
 
17
11
  # question types
18
12
  require 'ruql/quiz'
metadata CHANGED
@@ -1,43 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Armando Fox
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-30 00:00:00.000000000 Z
11
+ date: 2016-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ~>
16
+ - - '>='
17
17
  - !ruby/object:Gem::Version
18
18
  version: '3.0'
19
19
  prerelease: false
20
20
  name: builder
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - ~>
23
+ - - '>='
24
24
  - !ruby/object:Gem::Version
25
25
  version: '3.0'
26
26
  type: :runtime
27
27
  - !ruby/object:Gem::Dependency
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - '='
30
+ - - '>='
31
31
  - !ruby/object:Gem::Version
32
- version: 1.4.2
32
+ version: '1.0'
33
33
  prerelease: false
34
34
  name: getopt
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - '='
37
+ - - '>='
38
38
  - !ruby/object:Gem::Version
39
- version: 1.4.2
39
+ version: '1.0'
40
40
  type: :runtime
41
+ - !ruby/object:Gem::Dependency
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ prerelease: false
48
+ name: rspec
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ type: :development
55
+ - !ruby/object:Gem::Dependency
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '4.0'
61
+ prerelease: false
62
+ name: activesupport
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: '4.0'
68
+ type: :development
69
+ - !ruby/object:Gem::Dependency
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ prerelease: false
76
+ name: byebug
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
41
83
  description: Ruby-embedded DSL for creating short-answer quiz questions
42
84
  email: fox@cs.berkeley.edu
43
85
  executables:
@@ -65,7 +107,6 @@ files:
65
107
  - lib/ruql/renderers/html_form_renderer.rb
66
108
  - lib/ruql/renderers/json_renderer.rb
67
109
  - lib/ruql/renderers/qualtrics_renderer.rb
68
- - lib/ruql/renderers/xml_renderer.rb
69
110
  - lib/ruql/select_multiple.rb
70
111
  - lib/ruql/tex_output.rb
71
112
  - lib/ruql/true_false.rb
@@ -1,148 +0,0 @@
1
- class XMLRenderer
2
- require 'builder'
3
-
4
- attr_reader :output
5
- def initialize(quiz,options={})
6
- @output = ''
7
- @b = Builder::XmlMarkup.new(:target => @output, :indent => 2)
8
- @quiz = quiz
9
- end
10
-
11
- def render(thing)
12
- case thing
13
- when MultipleChoice,SelectMultiple,TrueFalse then render_multiple_choice(thing)
14
- when FillIn then render_fill_in(thing)
15
- else
16
- raise "Unknown question type: #{thing}"
17
- end
18
- end
19
-
20
- def render_quiz
21
- # entire quiz can be in one question group, as long as we specify
22
- # that ALL question from the group must be used to make the quiz.
23
- xml_quiz do
24
- # after preamble...
25
- @b.question_groups do
26
- @b.question_group(:select => @quiz.questions.length) do
27
- @quiz.questions.each do |question|
28
- self.render(question)
29
- end
30
- end
31
- end
32
- end
33
- @output
34
- end
35
-
36
- def render_fill_in(question)
37
- @b.question :type => 'GS_Short_Answer_Question_Simple', :id => question.object_id.to_s(16) do
38
- @b.metadata {
39
- @b.parameters {
40
- @b.rescale_score question.points
41
- @b.type 'regexp'
42
- }
43
- }
44
- # since we want all the options to appear, we create N option
45
- # groups each containig 1 option, and specify that option to
46
- # always be selected for inclusion in the quiz. If the original
47
- # question specified 'random', use the 'randomize' attribute on
48
- # option_groups to scramble the order in which displayed;
49
- # otherwise, display in same order as answers appear in source.
50
- @b.data {
51
- @b.text { @b.cdata!(question.question_text) }
52
- @b.option_groups(:randomize => !!question.randomize) {
53
- @b.option_group(:select => 'all') {
54
- question.answers.each do |answer|
55
- option_args = {}
56
- option_args['selected_score'] = answer.correct? ? 1 : 0
57
- option_args['unselected_score'] =
58
- question.multiple ? 1 - option_args['selected_score'] : 0
59
- option_args['id'] = answer.object_id.to_s(16)
60
- @b.option(option_args) do
61
- answer_text = answer.answer_text
62
- if answer_text.kind_of?(Regexp)
63
- answer_text = answer_text.inspect
64
- if !question.case_sensitive
65
- answer_text += 'i'
66
- end
67
- end
68
- @b.text { @b.cdata!(answer_text) }
69
- @b.explanation { @b.cdata!(answer.explanation) } if answer.has_explanation?
70
- end
71
- end
72
- }
73
- }
74
- }
75
- end
76
- end
77
-
78
- def render_multiple_choice(question)
79
- @b.question :type => 'GS_Choice_Answer_Question', :id => question.object_id.to_s(16) do
80
- @b.metadata {
81
- @b.parameters {
82
- @b.rescale_score question.points
83
- @b.choice_type (question.multiple ? 'checkbox' : 'radio')
84
- }
85
- }
86
- # since we want all the options to appear, we create N option
87
- # groups each containig 1 option, and specify that option to
88
- # always be selected for inclusion in the quiz. If the original
89
- # question specified 'random', use the 'randomize' attribute on
90
- # option_groups to scramble the order in which displayed;
91
- # otherwise, display in same order as answers appear in source.
92
- @b.data {
93
- @b.text { @b.cdata!(question.question_text) }
94
- @b.option_groups(:randomize => !!question.randomize) {
95
- question.answers.each do |a|
96
- @b.option_group(:select => 'all') {
97
- self.render_multiple_choice_answer a, question.multiple
98
- }
99
- end
100
- }
101
- }
102
- end
103
- end
104
- alias :render_true_false :render_multiple_choice
105
-
106
- def render_multiple_choice_answer(answer, multiple_allowed)
107
- option_args = {}
108
- option_args['selected_score'] = answer.correct? ? 1 : 0
109
- option_args['unselected_score'] =
110
- multiple_allowed ? 1 - option_args['selected_score'] : 0
111
- option_args['id'] = answer.object_id.to_s(16)
112
- @b.option(option_args) do
113
- @b.text { @b.cdata!(answer.answer_text) }
114
- @b.explanation { @b.cdata!(answer.explanation) } if answer.has_explanation?
115
- end
116
- end
117
-
118
- private
119
-
120
- def options_to_xml(h)
121
- h.each_pair do |k,v|
122
- if v.is_a?(Hash)
123
- @b.__send__(k.to_sym) do
124
- options_to_xml v
125
- end
126
- else
127
- @b.__send__(k.to_sym, v)
128
- end
129
- end
130
- end
131
-
132
-
133
- def xml_quiz
134
- @b.quiz do
135
- @b.metadata do
136
- @b.type 'quiz'
137
- @b.title @quiz.title
138
- options_to_xml @quiz.options
139
- end
140
- @b.preamble
141
- @b.data do
142
- yield
143
- end
144
- end
145
- end
146
-
147
- end
148
-