coradoc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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