coradoc 0.1.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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.docker/Dockerfile +19 -0
  3. data/.docker/Makefile +35 -0
  4. data/.docker/docker-compose.yml +14 -0
  5. data/.docker/readme.md +61 -0
  6. data/.hound.yml +5 -0
  7. data/.rubocop.yml +10 -0
  8. data/CHANGELOG.md +5 -0
  9. data/CODE_OF_CONDUCT.md +84 -0
  10. data/Gemfile +5 -0
  11. data/LICENSE.txt +21 -0
  12. data/Makefile +1 -0
  13. data/README.md +69 -0
  14. data/Rakefile +7 -0
  15. data/coradoc.gemspec +38 -0
  16. data/docker-compose.yml +1 -0
  17. data/lib/coradoc/document/admonition.rb +11 -0
  18. data/lib/coradoc/document/attribute.rb +27 -0
  19. data/lib/coradoc/document/author.rb +11 -0
  20. data/lib/coradoc/document/base.rb +17 -0
  21. data/lib/coradoc/document/bibdata.rb +24 -0
  22. data/lib/coradoc/document/block.rb +34 -0
  23. data/lib/coradoc/document/header.rb +11 -0
  24. data/lib/coradoc/document/list.rb +14 -0
  25. data/lib/coradoc/document/paragraph.rb +19 -0
  26. data/lib/coradoc/document/revision.rb +11 -0
  27. data/lib/coradoc/document/section.rb +28 -0
  28. data/lib/coradoc/document/table.rb +20 -0
  29. data/lib/coradoc/document/text_element.rb +22 -0
  30. data/lib/coradoc/document/title.rb +33 -0
  31. data/lib/coradoc/document.rb +46 -0
  32. data/lib/coradoc/legacy_parser.rb +200 -0
  33. data/lib/coradoc/oscal.rb +85 -0
  34. data/lib/coradoc/parser/asciidoc/base.rb +84 -0
  35. data/lib/coradoc/parser/asciidoc/bibdata.rb +19 -0
  36. data/lib/coradoc/parser/asciidoc/content.rb +143 -0
  37. data/lib/coradoc/parser/asciidoc/header.rb +30 -0
  38. data/lib/coradoc/parser/asciidoc/section.rb +60 -0
  39. data/lib/coradoc/parser/base.rb +32 -0
  40. data/lib/coradoc/parser.rb +11 -0
  41. data/lib/coradoc/transformer.rb +178 -0
  42. data/lib/coradoc/version.rb +5 -0
  43. data/lib/coradoc.rb +19 -0
  44. data/todo.md +10 -0
  45. metadata +174 -0
@@ -0,0 +1,33 @@
1
+ module Coradoc
2
+ module Document
3
+ class Title
4
+ attr_reader :id, :content, :line_break
5
+
6
+ def initialize(content, level, options = {})
7
+ @level_str = level
8
+ @content = content.to_s
9
+ @id = options.fetch(:id, nil).to_s
10
+ @line_break = options.fetch(:line_break, "")
11
+ end
12
+
13
+ def level
14
+ @level ||= level_from_string
15
+ end
16
+
17
+ alias :text :content
18
+
19
+ private
20
+
21
+ attr_reader :level_str
22
+
23
+ def level_from_string
24
+ case @level_str.length
25
+ when 2 then :heading_two
26
+ when 3 then :heading_three
27
+ when 4 then :heading_four
28
+ else :unknown
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ require "coradoc/document/title"
2
+ require "coradoc/document/block"
3
+ require "coradoc/document/section"
4
+ require "coradoc/document/attribute"
5
+ require "coradoc/document/admonition"
6
+ require "coradoc/document/text_element"
7
+ require "coradoc/document/author"
8
+ require "coradoc/document/revision"
9
+ require "coradoc/document/header"
10
+ require "coradoc/document/bibdata"
11
+ require "coradoc/document/paragraph"
12
+ require "coradoc/document/table"
13
+ require "coradoc/document/list"
14
+
15
+ module Coradoc
16
+ module Document
17
+ class << self
18
+ attr_reader :header, :bibdata, :sections
19
+
20
+ def from_adoc(filename)
21
+ ast = Coradoc::Parser.parse(filename)
22
+ Coradoc::Transformer.transform(ast)
23
+ end
24
+
25
+ def from_ast(elements)
26
+ @sections = []
27
+
28
+ elements.each do |element|
29
+ if element.is_a?(Coradoc::Document::Bibdata)
30
+ @bibdata = element
31
+ end
32
+
33
+ if element.is_a?(Coradoc::Document::Header)
34
+ @header = element
35
+ end
36
+
37
+ if element.is_a?(Coradoc::Document::Section)
38
+ @sections << element
39
+ end
40
+ end
41
+
42
+ self
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,200 @@
1
+ require "parslet"
2
+ require "parslet/convenience"
3
+
4
+ module Coradoc
5
+ class LegacyParser < Parslet::Parser
6
+ root :document
7
+
8
+ # Basic Elements
9
+ rule(:space) { match('\s') }
10
+ rule(:space?) { spaces.maybe }
11
+ rule(:spaces) { space.repeat(1) }
12
+ rule(:empty_line) { match("^\n") }
13
+
14
+ rule(:endline) { newline | any.absent? }
15
+ rule(:newline) { match["\r\n"].repeat(1) }
16
+ rule(:line_ending) { match("[\n]") }
17
+
18
+ rule(:inline_element) { text }
19
+ rule(:text) { match("[^\n]").repeat(1) }
20
+ rule(:digits) { match("[0-9]").repeat(1) }
21
+ rule(:word) { match("[a-zA-Z0-9_-]").repeat(1) }
22
+ rule(:special_character) { match("^[*_:=-]") | str("[#") }
23
+
24
+ rule(:text_line) do
25
+ special_character.absent? >>
26
+ match("[^\n]").repeat(1).as(:text) >>
27
+ line_ending.as(:break)
28
+ end
29
+
30
+ # Common Helpers
31
+ rule(:words) { word >> (space? >> word).repeat }
32
+ rule(:email) { word >> str("@") >> word >> str(".") >> word }
33
+
34
+ # Document
35
+ rule(:document) do
36
+ (
37
+ bibdata.repeat(1).as(:bibdata) |
38
+ section.as(:section) |
39
+ header.as(:header) |
40
+ block_with_title.as(:block) |
41
+ empty_line.repeat(1) |
42
+ any.as(:unparsed)
43
+ ).repeat(1).as(:document)
44
+ end
45
+
46
+ # Header
47
+ rule(:header) do
48
+ match("=") >> space? >> text.as(:title) >> newline >>
49
+ author.maybe.as(:author) >> revision.maybe.as(:revision)
50
+ end
51
+
52
+ rule(:author) do
53
+ words.as(:first_name) >> str(",") >> space? >> words.as(:last_name) >>
54
+ space? >> str("<") >> email.as(:email) >> str(">") >> endline
55
+ end
56
+
57
+ rule(:revision) do
58
+ (word >> (str(".") >> word).maybe).as(:number) >>
59
+ str(",") >> space? >> word.as(:date) >>
60
+ str(":") >> space? >> words.as(:remark) >> newline
61
+ end
62
+
63
+ # Bibdata
64
+ rule(:bibdata) do
65
+ str(":") >> attribute_name.as(:key) >> str(":") >>
66
+ space? >> attribute_value.as(:value) >> endline
67
+ end
68
+
69
+ # Section
70
+ rule(:section) do
71
+ heading.as(:title) >>
72
+ (list.as(:list) |
73
+ blocks.as(:blocks) |
74
+ paragraphs.as(:paragraphs)).maybe
75
+ end
76
+
77
+ # Heading
78
+ rule(:heading) do
79
+ (anchor_name >> newline).maybe >>
80
+ match("=").repeat(2, 8).as(:level) >>
81
+ space? >> text.as(:text) >> endline.as(:break)
82
+ end
83
+
84
+ rule(:anchor_name) { str("[#") >> keyword.as(:name) >> str("]") }
85
+
86
+ # List
87
+ rule(:list) do
88
+ unnumbered_list.as(:unnumbered) |
89
+ definition_list.as(:definition) | numbered_list.as(:numbered)
90
+ end
91
+
92
+ rule(:numbered_list) { nlist_item.repeat(1) }
93
+ rule(:unnumbered_list) { ulist_item.repeat(1) }
94
+ rule(:definition_list) { dlist_item.repeat(1) }
95
+
96
+ rule(:nlist_item) { match("\.") >> space >> text_line }
97
+ rule(:ulist_item) { match("\\*") >> space >> text_line }
98
+ rule(:dlist_item) do
99
+ str("term") >> space >> digits >> str("::") >> space >> text_line
100
+ end
101
+
102
+ # Block
103
+ rule(:block) { simple_block | open_block }
104
+ rule(:attribute_name) { keyword }
105
+ rule(:attribute_value) { text | str("") }
106
+ rule(:keyword) { match("[a-zA-Z0-9_-]").repeat(1) }
107
+ rule(:blocks) { block.repeat(1) >> (newline >> block.repeat(1)).maybe }
108
+
109
+ rule(:block_title) { str(".") >> text.as(:title) >> line_ending }
110
+ rule(:block_type) { str("[") >> keyword.as(:type) >> str("]") >> newline }
111
+
112
+ rule(:block_attribute) do
113
+ str("[") >> keyword.as(:key) >>
114
+ str("=") >> keyword.as(:value) >> str("]")
115
+ end
116
+
117
+ rule(:simple_block) do
118
+ block_attribute.as(:attributes) >> newline >>
119
+ text_line.repeat(1).as(:lines)
120
+ end
121
+
122
+ rule(:open_block) do
123
+ block_title >>
124
+ block_type >>
125
+ str("--").as(:delimiter) >> newline >>
126
+ text_line.repeat.as(:lines) >>
127
+ str("--") >> line_ending
128
+ end
129
+
130
+ rule(:example_block) do
131
+ block_title >>
132
+ block_type >>
133
+ str("====").as(:delimiter) >> newline >>
134
+ text_line.repeat(1).as(:lines) >>
135
+ str("====") >> newline
136
+ end
137
+
138
+ rule(:sidebar_block) do
139
+ block_title >>
140
+ block_type.maybe >>
141
+ str("****").as(:delimiter) >> newline >>
142
+ text_line.repeat(1).as(:lines) >>
143
+ str("****") >> newline
144
+ end
145
+
146
+ rule(:source_block) do
147
+ block_title >>
148
+ str("----").as(:delimiter) >> newline >>
149
+ text_line.repeat(1).as(:lines) >>
150
+ str("----") >> newline
151
+ end
152
+
153
+ rule(:quote_block) do
154
+ block_title >>
155
+ str("____").as(:delimiter) >> newline >>
156
+ text_line.repeat.as(:lines) >>
157
+ str("____") >> newline
158
+ end
159
+
160
+ rule(:block_with_title) do
161
+ example_block | quote_block |
162
+ sidebar_block | source_block | open_block |
163
+ (block_title >> text_line.repeat(1).as(:lines))
164
+ end
165
+
166
+ # Paragraph
167
+ rule(:paragraphs) do
168
+ paragraph >> (line_ending.repeat(1) >> paragraph).repeat.maybe
169
+ end
170
+
171
+ rule(:paragraph) { admonitions.repeat(1) | text_line.repeat(1) }
172
+
173
+ # Admonition
174
+ rule(:admonition_type) do
175
+ (str("NOTE") |
176
+ str("TIP") |
177
+ str("EDITOR") |
178
+ str("DANGER") |
179
+ str("CAUTION") |
180
+ str("WARNING") |
181
+ str("IMPORTANT")).as(:type)
182
+ end
183
+
184
+ rule(:admonitions) { admonition.as(:admonition).repeat(1) }
185
+ rule(:admonition) { inline_admonition | block_admonition }
186
+
187
+ rule(:inline_admonition) do
188
+ admonition_type >> str(":") >> space? >> text_line >> newline
189
+ end
190
+
191
+ rule(:block_admonition) do
192
+ str("[") >> admonition_type >> str("]") >> newline >> text_line >> newline
193
+ end
194
+
195
+ def self.parse(filename)
196
+ content = File.read(filename)
197
+ new.parse_with_debug(content)
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,85 @@
1
+ require "yaml"
2
+
3
+ module Coradoc
4
+ class Oscal
5
+ attr_reader :_doc
6
+
7
+ def initialize(document)
8
+ @_doc = document
9
+ end
10
+
11
+ def self.to_oscal(document)
12
+ new(document).to_oscal
13
+ end
14
+
15
+ def to_oscal
16
+ {"metadata" => _doc.bibdata.to_hash, "groups" => sections_as_groups}
17
+ end
18
+
19
+ private
20
+
21
+ # Organizational controls
22
+ def sections_as_groups
23
+ _doc.sections.map do |section|
24
+ Hash.new.tap do |hash|
25
+ hash["id"] = section.id
26
+ hash["title"] = section.title&.content
27
+ hash["controls"] = build_oscal_controls(section.sections)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Clause 5.1
33
+ def build_oscal_controls(sections)
34
+ sections.map do |section|
35
+ Hash.new.tap do |hash|
36
+ hash["id"] = section.id
37
+ hash["props"] = build_oscal_props(section.glossaries.items)
38
+ hash["parts"] = build_oscal_parts(section.sections)
39
+ end
40
+ end
41
+ end
42
+
43
+ # Control, Purpose, Guidance
44
+ def build_oscal_parts(sections)
45
+ sections.map do |section|
46
+ Hash.new.tap do |hash|
47
+ hash["id"] = section.id
48
+ hash["name"] = section.title&.text
49
+ hash["prose"] = build_oscal_prose(section.content)
50
+ hash["parts"] = build_oscal_sub_parts(section.contents)
51
+ end.compact
52
+ end
53
+ end
54
+
55
+ def build_oscal_sub_parts(contents)
56
+ if contents.length > 1
57
+ parts = contents.select do |content|
58
+ content if content.is_a?(Coradoc::Document::Paragraph)
59
+ end
60
+
61
+ parts.map do |part|
62
+ Hash.new.tap do |hash|
63
+ hash["id"] = part.id
64
+ hash["prose"] = part.texts.join(" ")
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def build_oscal_props(attributes)
71
+ attributes.map do |attribute|
72
+ Hash.new.tap do |hash|
73
+ hash["name"] = attribute.key.to_s.downcase
74
+ hash["value"] = attribute.value
75
+ end
76
+ end
77
+ end
78
+
79
+ def build_oscal_prose(paragraph)
80
+ if paragraph
81
+ paragraph.texts.join(" ")
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,84 @@
1
+ module Coradoc
2
+ module Parser
3
+ module Asciidoc
4
+ module Base
5
+ def space?
6
+ space.maybe
7
+ end
8
+
9
+ def space
10
+ match('\s').repeat(1)
11
+ end
12
+
13
+ def text
14
+ match("[^\n]").repeat(1)
15
+ end
16
+
17
+ def line_ending
18
+ match("[\n]")
19
+ end
20
+
21
+ def endline
22
+ newline | any.absent?
23
+ end
24
+
25
+ def newline
26
+ match["\r\n"].repeat(1)
27
+ end
28
+
29
+ # def line_break
30
+ # match["\r\n"]
31
+ # end
32
+
33
+ def keyword
34
+ (match("[a-zA-Z0-9_-]") | str(".")).repeat(1)
35
+ end
36
+
37
+ # def text_line
38
+ # special_character.absent? >>
39
+ # match("[^\n]").repeat(1).as(:text) >>
40
+ # line_ending.as(:break)
41
+ # end
42
+
43
+ # rule(:space) { match('\s') }
44
+ # rule(:space?) { spaces.maybe }
45
+ # rule(:spaces) { space.repeat(1) }
46
+ def empty_line
47
+ match("^\n")
48
+ end
49
+ #
50
+
51
+ #
52
+ # rule(:inline_element) { text }
53
+ # rule(:text) { match("[^\n]").repeat(1) }
54
+ def digits
55
+ match("[0-9]").repeat(1)
56
+ end
57
+
58
+ def word
59
+ match("[a-zA-Z0-9_-]").repeat(1)
60
+ end
61
+
62
+ def words
63
+ word >> (space? >> word).repeat
64
+ end
65
+
66
+ def email
67
+ word >> str("@") >> word >> str(".") >> word
68
+ end
69
+
70
+ def attribute_name
71
+ match("[a-zA-Z0-9_-]").repeat(1)
72
+ end
73
+
74
+ def attribute_value
75
+ text | str("")
76
+ end
77
+
78
+ def special_character
79
+ match("^[*_:=-]") | str("[#") | str("[[")
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,19 @@
1
+ module Coradoc
2
+ module Parser
3
+ module Asciidoc
4
+ module Bibdata
5
+ include Coradoc::Parser::Asciidoc::Base
6
+
7
+ # Bibdata
8
+ def bibdatas
9
+ bibdata.repeat(1)
10
+ end
11
+
12
+ def bibdata
13
+ str(":") >> attribute_name.as(:key) >> str(":") >>
14
+ space? >> attribute_value.as(:value) >> line_ending
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,143 @@
1
+ module Coradoc
2
+ module Parser
3
+ module Asciidoc
4
+ module Content
5
+ include Coradoc::Parser::Asciidoc::Base
6
+
7
+ def paragraph
8
+ text_line.repeat(1)
9
+ end
10
+
11
+ def glossaries
12
+ glossary.repeat(1)
13
+ end
14
+
15
+ # List
16
+ def list
17
+ unnumbered_list.as(:unnumbered) |
18
+ definition_list.as(:definition) | numbered_list.as(:numbered)
19
+ end
20
+
21
+ def contents
22
+ (
23
+ example_block.as(:example) |
24
+ list.as(:list) |
25
+ table.as(:table) |
26
+ highlight.as(:highlight) |
27
+ glossaries.as(:glossaries) |
28
+ paragraph.as(:paragraph) | empty_line
29
+ ).repeat(1)
30
+ end
31
+
32
+ def example_block
33
+ str("[example]") >> newline >>
34
+ str("=").repeat(4).capture(:delimiter) >> newline >>
35
+ dynamic do |source, context|
36
+ (str(context.captures[:delimiter]).absent? >> text.as(:text) >> endline).repeat(1) >>
37
+ str(context.captures[:delimiter]) >> endline
38
+ end
39
+ end
40
+
41
+ def highlight
42
+ text_id >> newline >>
43
+ underline >> highlight_text >> newline
44
+ end
45
+
46
+ def underline
47
+ str("[underline]") | str("[.underline]")
48
+ end
49
+
50
+ def highlight_text
51
+ str("#") >> words.as(:text) >> str("#")
52
+ end
53
+
54
+ # Table
55
+ def table
56
+ block_title >>
57
+ str("|===") >> line_ending >>
58
+ table_row.repeat(1).as(:rows) >>
59
+ str("|===") >> line_ending
60
+ end
61
+
62
+ def table_row
63
+ (literal_space? >> str("|") >> (cell_content | empty_cell_content)).
64
+ repeat(1).as(:cols) >> line_ending
65
+ end
66
+
67
+ # Extended
68
+ def word
69
+ (match("[a-zA-Z0-9_-]") | str(".") | str("*") | match("@")).repeat(1)
70
+ end
71
+
72
+ def empty_cell_content
73
+ str("|").absent? >> literal_space.as(:text)
74
+ end
75
+
76
+ def cell_content
77
+ str("|").absent? >> literal_space? >> words.as(:text)
78
+ end
79
+
80
+ def literal_space
81
+ (match[' '] | match[' \t']).repeat(1)
82
+ end
83
+
84
+ # Override
85
+ def literal_space?
86
+ literal_space.maybe
87
+ end
88
+
89
+ def block_title
90
+ str(".") >> text.as(:title) >> line_ending
91
+ end
92
+
93
+ # Text
94
+ def text_line
95
+ (asciidoc_char_with_id.absent? | text_id) >> literal_space? >>
96
+ text.as(:text) >> line_ending.as(:break)
97
+ end
98
+
99
+ def asciidoc_char
100
+ match("^[*_:=-]")
101
+ end
102
+
103
+ def asciidoc_char_with_id
104
+ asciidoc_char | str("[#") | str("[[")
105
+ end
106
+
107
+ def text_id
108
+ str("[[") >> keyword.as(:id) >> str("]]") |
109
+ str("[#") >> keyword.as(:id) >> str("]")
110
+ end
111
+
112
+ def glossary
113
+ keyword.as(:key) >> str("::") >> space? >>
114
+ text.as(:value) >> line_ending.as(:break)
115
+ end
116
+
117
+ def numbered_list
118
+ nlist_item.repeat(1)
119
+ end
120
+
121
+ def unnumbered_list
122
+ (ulist_item >> newline.maybe).repeat(1)
123
+ end
124
+
125
+ def definition_list
126
+ dlist_item.repeat(1)
127
+ end
128
+
129
+ def nlist_item
130
+ match("\.") >> space >> text_line
131
+ end
132
+
133
+ def ulist_item
134
+ match("\\*") >> space >> text_line
135
+ end
136
+
137
+ def dlist_item
138
+ str("term") >> space >> digits >> str("::") >> space >> text_line
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,30 @@
1
+ require "coradoc/parser/asciidoc/base"
2
+
3
+ module Coradoc
4
+ module Parser
5
+ module Asciidoc
6
+ module Header
7
+ include Coradoc::Parser::Asciidoc::Base
8
+
9
+ # Header
10
+ def header
11
+ match("=") >> space? >> text.as(:title) >> newline >>
12
+ author.maybe.as(:author) >> revision.maybe.as(:revision)
13
+ end
14
+
15
+ # Author
16
+ def author
17
+ words.as(:first_name) >> str(",") >> space? >> words.as(:last_name) >>
18
+ space? >> str("<") >> email.as(:email) >> str(">") >> endline
19
+ end
20
+
21
+ # Revision
22
+ def revision
23
+ (word >> (str(".") >> word).maybe).as(:number) >>
24
+ str(",") >> space? >> word.as(:date) >>
25
+ str(":") >> space? >> words.as(:remark) >> newline
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,60 @@
1
+ require "coradoc/parser/asciidoc/base"
2
+ require "coradoc/parser/asciidoc/content"
3
+
4
+ module Coradoc
5
+ module Parser
6
+ module Asciidoc
7
+ module Section
8
+ include Coradoc::Parser::Asciidoc::Base
9
+ include Coradoc::Parser::Asciidoc::Content
10
+
11
+ def section_block(level = 2)
12
+ section_id.maybe >>
13
+ section_title(level).as(:title) >>
14
+ contents.as(:contents).maybe
15
+ end
16
+
17
+ # Section id
18
+ def section_id
19
+ (str("[[") >> keyword.as(:id) >> str("]]") |
20
+ str("[#") >> keyword.as(:id) >> str("]")) >> newline
21
+ end
22
+
23
+ # Heading
24
+ def section_title(level = 2, max_level = 8)
25
+ match("=").repeat(level, max_level).as(:level) >>
26
+ space? >> text.as(:text) >> endline.as(:break)
27
+ end
28
+
29
+ # section
30
+ def section
31
+ section_block >> second_level_section.repeat.maybe.as(:sections)
32
+ end
33
+
34
+ def sub_section(level)
35
+ newline.maybe >> section_block(level)
36
+ end
37
+
38
+ def second_level_section
39
+ sub_section(3) >> third_level_section.repeat.maybe.as(:sections)
40
+ end
41
+
42
+ def third_level_section
43
+ sub_section(4) >> fourth_level_section.repeat.maybe.as(:sections)
44
+ end
45
+
46
+ def fourth_level_section
47
+ sub_section(5) >> fifth_level_section.repeat.maybe.as(:sections)
48
+ end
49
+
50
+ def fifth_level_section
51
+ sub_section(6) >> sixth_level_section.repeat.maybe.as(:sections)
52
+ end
53
+
54
+ def sixth_level_section
55
+ sub_section(7) >> sub_section(8).repeat.maybe.as(:sections)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end