brief 0.0.1 → 0.0.2
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 +5 -13
- data/Gemfile +4 -0
- data/Gemfile.lock +81 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +71 -0
- data/Rakefile +11 -0
- data/bin/brief +35 -0
- data/brief-0.0.1.gem +0 -0
- data/brief.gemspec +33 -0
- data/examples/project_overview.md +23 -0
- data/lib/brief/cli/commands/config.rb +40 -0
- data/lib/brief/cli/commands/publish.rb +27 -0
- data/lib/brief/cli/commands/write.rb +26 -0
- data/lib/brief/configuration.rb +134 -0
- data/lib/brief/document.rb +68 -0
- data/lib/brief/dsl.rb +0 -0
- data/lib/brief/formatters/base.rb +12 -0
- data/lib/brief/formatters/github_milestone_rollup.rb +52 -0
- data/lib/brief/git.rb +19 -0
- data/lib/brief/github/wiki.rb +9 -0
- data/lib/brief/github.rb +78 -0
- data/lib/brief/github_client/authentication.rb +32 -0
- data/lib/brief/github_client/client.rb +86 -0
- data/lib/brief/github_client/commands.rb +5 -0
- data/lib/brief/github_client/issue_labels.rb +65 -0
- data/lib/brief/github_client/issues.rb +22 -0
- data/lib/brief/github_client/milestone_issues.rb +13 -0
- data/lib/brief/github_client/organization_activity.rb +9 -0
- data/lib/brief/github_client/organization_issues.rb +13 -0
- data/lib/brief/github_client/organization_repositories.rb +20 -0
- data/lib/brief/github_client/organization_users.rb +9 -0
- data/lib/brief/github_client/repository_events.rb +8 -0
- data/lib/brief/github_client/repository_issue_events.rb +9 -0
- data/lib/brief/github_client/repository_issues.rb +8 -0
- data/lib/brief/github_client/repository_labels.rb +18 -0
- data/lib/brief/github_client/repository_milestones.rb +9 -0
- data/lib/brief/github_client/request.rb +181 -0
- data/lib/brief/github_client/request_wrapper.rb +121 -0
- data/lib/brief/github_client/response_object.rb +50 -0
- data/lib/brief/github_client/single_repository.rb +9 -0
- data/lib/brief/github_client/user_activity.rb +16 -0
- data/lib/brief/github_client/user_gists.rb +9 -0
- data/lib/brief/github_client/user_info.rb +9 -0
- data/lib/brief/github_client/user_issues.rb +13 -0
- data/lib/brief/github_client/user_organizations.rb +9 -0
- data/lib/brief/github_client/user_repositories.rb +9 -0
- data/lib/brief/github_client.rb +43 -0
- data/lib/brief/handlers/base.rb +62 -0
- data/lib/brief/handlers/github_issue.rb +41 -0
- data/lib/brief/handlers/github_milestone.rb +37 -0
- data/lib/brief/handlers/github_wiki.rb +11 -0
- data/lib/brief/line.rb +69 -0
- data/lib/brief/parser.rb +354 -0
- data/lib/brief/publisher/handler_manager.rb +47 -0
- data/lib/brief/publisher.rb +142 -0
- data/lib/brief/tree.rb +42 -0
- data/lib/brief/version.rb +9 -0
- data/lib/brief.rb +56 -0
- data/lib/core_ext.rb +37 -0
- data/spec/fixtures/front_end_tutorial.md +33 -0
- data/spec/fixtures/generated/project_overview.json +0 -0
- data/spec/fixtures/generator_dsl_example.rb +22 -0
- data/spec/fixtures/project_overview.md +48 -0
- data/spec/fixtures/sample.md +19 -0
- data/spec/lib/brief/document_spec.rb +35 -0
- data/spec/lib/brief/dsl_spec.rb +21 -0
- data/spec/lib/brief/line_spec.rb +11 -0
- data/spec/lib/brief/parser_spec.rb +12 -0
- data/spec/spec_helper.rb +25 -0
- metadata +231 -9
data/lib/brief/parser.rb
ADDED
@@ -0,0 +1,354 @@
|
|
1
|
+
# This is my first attempt at breaking down raw markdown
|
2
|
+
# into a tree / hierarchy, so that it can be used to do other
|
3
|
+
# things / create other entities.
|
4
|
+
#
|
5
|
+
module Brief
|
6
|
+
class Parser
|
7
|
+
attr_accessor :content, :options, :raw_tree, :checksum
|
8
|
+
|
9
|
+
# Regexes
|
10
|
+
HeadingRegex = /^#+/
|
11
|
+
|
12
|
+
# Exceptions
|
13
|
+
MissingHeadings = Class.new(Exception)
|
14
|
+
HeadingsNotUnique = Class.new(Exception)
|
15
|
+
|
16
|
+
def initialize(content="",options={})
|
17
|
+
@options = options.dup
|
18
|
+
@content = content
|
19
|
+
@checksum = options.fetch(:checksum, Digest::MD5.hexdigest(content))
|
20
|
+
|
21
|
+
scan
|
22
|
+
parse
|
23
|
+
end
|
24
|
+
|
25
|
+
def tree
|
26
|
+
@tree ||= Brief::Tree.new(tree_nodes.dup.freeze)
|
27
|
+
end
|
28
|
+
|
29
|
+
def items(sorted=true)
|
30
|
+
sorted ? tree.items.sort_by(&:sort_index) : tree.items
|
31
|
+
end
|
32
|
+
|
33
|
+
def headings_at_level level, options={}
|
34
|
+
as_objects = options.fetch(:as_objects, false)
|
35
|
+
items = heading_lines.select {|line| line.level == level }.sort_by(&:sort_index)
|
36
|
+
as_objects ? items : items.map(&:content)
|
37
|
+
end
|
38
|
+
|
39
|
+
def next_sibling_of(heading_line)
|
40
|
+
if heading_line.is_a?(String)
|
41
|
+
heading_line = heading_lines.detect {|h| h.content == heading_line }
|
42
|
+
end
|
43
|
+
|
44
|
+
heading_lines.detect {|l| l.level <= heading_line.level && l.number > heading_line.number }
|
45
|
+
end
|
46
|
+
|
47
|
+
def body_content_for(heading_line)
|
48
|
+
if heading_line.is_a?(String)
|
49
|
+
heading_line = heading_lines.detect {|h| h.content == heading_line }
|
50
|
+
end
|
51
|
+
|
52
|
+
min = heading_line.number + 1
|
53
|
+
max = next_sibling_of(heading_line).try(:number).try(:-, 1) || last_line_number
|
54
|
+
|
55
|
+
parsed_lines.select {|p| p.number.between?(min,max) }.map(&:raw).map(&:strip).join("")
|
56
|
+
end
|
57
|
+
|
58
|
+
def lines_between_boundaries *bounds
|
59
|
+
bounds.map do |range|
|
60
|
+
a = range.min - 1
|
61
|
+
b = range.max - a
|
62
|
+
parsed_lines.slice(a, b).map(&:raw)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def last_line_number
|
67
|
+
parsed_lines.last.try(:number) || raw_lines.length + 1
|
68
|
+
end
|
69
|
+
|
70
|
+
def parser
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def extract_frontmatter!
|
75
|
+
if raw_lines.any? {|l| l.match(/```settings/) }
|
76
|
+
markers = identify_codemarkers
|
77
|
+
|
78
|
+
if raw_lines[markers.first].to_s.match(/```settings/)
|
79
|
+
length = markers[1] - markers[0]
|
80
|
+
@front_matter = raw_lines.slice(markers[0] + 1, length - 1).join
|
81
|
+
remove_raw_lines(markers[0], markers[1])
|
82
|
+
@content = raw_lines.join.gsub(/^\n|\n$/,'')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def front_matter
|
88
|
+
YAML.load(@front_matter) rescue {}
|
89
|
+
end
|
90
|
+
|
91
|
+
def identify_codemarkers
|
92
|
+
markers = []
|
93
|
+
raw_lines.each_with_index do |line, index|
|
94
|
+
if line.match(Brief::Line::CodeBlockRegex)
|
95
|
+
markers << index
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
markers
|
100
|
+
end
|
101
|
+
|
102
|
+
def scan
|
103
|
+
extract_frontmatter!
|
104
|
+
|
105
|
+
@code_markers = identify_codemarkers
|
106
|
+
@heading_markers = identify_heading_markers
|
107
|
+
|
108
|
+
@scanned = true
|
109
|
+
end
|
110
|
+
|
111
|
+
def identify_heading_markers
|
112
|
+
heading_markers = []
|
113
|
+
raw_lines.each_with_index do |line, index|
|
114
|
+
if line.match(Brief::Line::HeadingRegex) && !is_line_inside_code_region?(index)
|
115
|
+
heading_markers << index
|
116
|
+
end
|
117
|
+
end
|
118
|
+
heading_markers
|
119
|
+
end
|
120
|
+
|
121
|
+
def heading_boundaries
|
122
|
+
@heading_markers && @heading_markers.each_slice(2).map do |slice|
|
123
|
+
Range.new slice.first + 1, slice.last + 1
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def code_boundaries
|
128
|
+
@code_markers && @code_markers.each_slice(2).map do |slice|
|
129
|
+
Range.new slice.first + 1, slice.last + 1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def valid?
|
134
|
+
@validated.presence || validate
|
135
|
+
end
|
136
|
+
|
137
|
+
def validate
|
138
|
+
scan unless @scanned
|
139
|
+
|
140
|
+
unless heading_lines.length > 0
|
141
|
+
raise MissingHeadings, 'A Brief must include some headings'
|
142
|
+
end
|
143
|
+
|
144
|
+
if heading_lines.uniq.length < heading_lines.length
|
145
|
+
raise HeadingsNotUnique, 'Headings inside a brief must be unique'
|
146
|
+
end
|
147
|
+
|
148
|
+
@validated = true
|
149
|
+
end
|
150
|
+
|
151
|
+
# This syntax isn't right, but it works.
|
152
|
+
# TODO: Research the best way to do this.
|
153
|
+
# middleman had an example of using the &method(:method_name)
|
154
|
+
# to treat a method definition as a proc
|
155
|
+
def tree_visitor node
|
156
|
+
subheadings = headings_under(node, as_objects: true)
|
157
|
+
children = subheadings.map(&method(:tree_visitor))
|
158
|
+
id = "#{ checksum }_#{ node.sort_index.join('_') }"
|
159
|
+
|
160
|
+
children.each_with_index do |child, index|
|
161
|
+
child[:heading_index] = index
|
162
|
+
end
|
163
|
+
|
164
|
+
base = {
|
165
|
+
id: id,
|
166
|
+
level: node.level,
|
167
|
+
children: children,
|
168
|
+
title: node.content,
|
169
|
+
sort_index: node.sort_index,
|
170
|
+
line_number: node.line_number
|
171
|
+
}
|
172
|
+
|
173
|
+
base.merge! content: body_content_for(node).to_s.strip if node.heading?
|
174
|
+
|
175
|
+
base
|
176
|
+
end
|
177
|
+
|
178
|
+
def tree_nodes
|
179
|
+
@tree_nodes ||= begin
|
180
|
+
nodes = []
|
181
|
+
|
182
|
+
headings_at_level(highest_level, as_objects: true).each_with_index do |node, index|
|
183
|
+
element = tree_visitor(node)
|
184
|
+
element[:heading_index] = index
|
185
|
+
nodes << element
|
186
|
+
end
|
187
|
+
|
188
|
+
nodes.map {|node| Hashie::Mash.new(node) }.flatten
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def elements
|
193
|
+
@elements ||= tree.elements
|
194
|
+
end
|
195
|
+
|
196
|
+
def maximum_level
|
197
|
+
elements.map(&:level).max
|
198
|
+
end
|
199
|
+
|
200
|
+
def line_at(number)
|
201
|
+
parsed_lines.detect {|line| line.number == number }
|
202
|
+
end
|
203
|
+
|
204
|
+
def index_of(line)
|
205
|
+
lines.index(line)
|
206
|
+
end
|
207
|
+
|
208
|
+
def next_heading_after heading, options={}
|
209
|
+
heading_lines.detect {|line| line.number > heading.number }
|
210
|
+
end
|
211
|
+
|
212
|
+
def highest_level
|
213
|
+
level_boundaries.last
|
214
|
+
end
|
215
|
+
|
216
|
+
def lowest_level
|
217
|
+
level_boundaries.first
|
218
|
+
end
|
219
|
+
|
220
|
+
def level_boundaries
|
221
|
+
levels = heading_lines.map(&:level).uniq
|
222
|
+
[levels.max, levels.min]
|
223
|
+
end
|
224
|
+
|
225
|
+
def heading_lines
|
226
|
+
parsed_lines.select do |line|
|
227
|
+
line.heading?
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def is_line_inside_code_region?(line_number)
|
232
|
+
code_boundaries.any? {|range| range.include?(line_number) }
|
233
|
+
end
|
234
|
+
|
235
|
+
def headings_after(heading_line)
|
236
|
+
if heading_line.is_a?(String)
|
237
|
+
heading_line = heading_lines.detect {|h| h.content == heading_line }
|
238
|
+
end
|
239
|
+
|
240
|
+
heading_lines.select {|line| line.number > heading_line.number }
|
241
|
+
end
|
242
|
+
|
243
|
+
def headings_under(heading_line, options={})
|
244
|
+
matches = []
|
245
|
+
continue = true
|
246
|
+
|
247
|
+
include_subtree = options.fetch(:all, false)
|
248
|
+
as_objects = options.fetch(:as_objects, false)
|
249
|
+
|
250
|
+
if heading_line.is_a?(String)
|
251
|
+
heading_line = heading_lines.detect {|h| h.content == heading_line }
|
252
|
+
end
|
253
|
+
|
254
|
+
headings_after(heading_line).each do |line|
|
255
|
+
continue = false if line.level <= heading_line.level
|
256
|
+
if continue && line.level > heading_line.level
|
257
|
+
matches << line unless (!include_subtree && line.level - heading_line.level > 1)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
as_objects ? matches : matches.map(&:content)
|
262
|
+
end
|
263
|
+
|
264
|
+
def content_lines_under(heading_line, options={})
|
265
|
+
reject_blank = !options.fetch(:include_blank, true)
|
266
|
+
as_objects = options.fetch(:as_objects, false)
|
267
|
+
|
268
|
+
if heading_line.is_a?(String)
|
269
|
+
heading_line = heading_lines.detect {|h| h.content == heading_line }
|
270
|
+
end
|
271
|
+
|
272
|
+
return [] unless heading_line.respond_to?(:number)
|
273
|
+
|
274
|
+
min = heading_line.number
|
275
|
+
max = next_heading_after(heading_line).try(:number) || last_line_number
|
276
|
+
|
277
|
+
matches = content_lines.select do |line|
|
278
|
+
line.content? && line.number.between?(min, max)
|
279
|
+
end
|
280
|
+
|
281
|
+
matches.reject! {|m| m.content.blank? } if reject_blank
|
282
|
+
|
283
|
+
as_objects ? matches : matches.map(&:content)
|
284
|
+
end
|
285
|
+
|
286
|
+
def content_lines
|
287
|
+
parsed_lines.select(&:content?)
|
288
|
+
end
|
289
|
+
|
290
|
+
def code_block_markers
|
291
|
+
parsed_lines.select {|l| l.type == "code_block_marker" }
|
292
|
+
end
|
293
|
+
|
294
|
+
def raw_lines
|
295
|
+
@raw_lines ||= @content.lines.to_a
|
296
|
+
end
|
297
|
+
|
298
|
+
# I really need to get better with arrays
|
299
|
+
def remove_raw_lines start_index, end_index
|
300
|
+
original = @raw_lines.dup
|
301
|
+
copy = []
|
302
|
+
|
303
|
+
original.each_with_index do |line, index|
|
304
|
+
copy.push(line) unless index.between? start_index, end_index
|
305
|
+
end
|
306
|
+
|
307
|
+
@raw_lines = copy
|
308
|
+
end
|
309
|
+
|
310
|
+
def stripped_lines
|
311
|
+
@stripped ||= raw_lines.map(&:strip)
|
312
|
+
end
|
313
|
+
|
314
|
+
def parsed_lines
|
315
|
+
@parsed_lines
|
316
|
+
end
|
317
|
+
|
318
|
+
def parse clear=false
|
319
|
+
@parsed_lines = nil if clear
|
320
|
+
|
321
|
+
return @parsed_lines if @parsed_lines
|
322
|
+
|
323
|
+
parsed = []
|
324
|
+
|
325
|
+
raw = raw_lines
|
326
|
+
|
327
|
+
stripped_lines.each_with_index do |line,index|
|
328
|
+
is_code = code_boundaries.any? {|bounds| bounds.include?(index) }
|
329
|
+
line = Brief::Line.new(line, index, is_code)
|
330
|
+
line.raw = raw[index]
|
331
|
+
parsed << line
|
332
|
+
end
|
333
|
+
|
334
|
+
@parsed_lines = parsed
|
335
|
+
end
|
336
|
+
|
337
|
+
def code_blocks_by_language
|
338
|
+
return @code_samples if @code_samples
|
339
|
+
|
340
|
+
bounds = Array(parser && parser.send(:code_boundaries))
|
341
|
+
sections = parser.lines_between_boundaries(*bounds)
|
342
|
+
|
343
|
+
@code_samples = sections.inject({}) do |memo,section|
|
344
|
+
marker = section.first.split('```').last
|
345
|
+
language = marker.nil? ? :text : marker.strip
|
346
|
+
|
347
|
+
section.shift && section.pop
|
348
|
+
memo[language.to_sym] ||= []
|
349
|
+
memo[language.to_sym] << section.join("").sub(/^\s+/,'')
|
350
|
+
memo
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Brief
|
2
|
+
class Publisher::HandlerManager
|
3
|
+
|
4
|
+
attr_reader :publisher, :document
|
5
|
+
|
6
|
+
def initialize(publisher, document)
|
7
|
+
@publisher = publisher
|
8
|
+
@document = document
|
9
|
+
end
|
10
|
+
|
11
|
+
def parser
|
12
|
+
@parser ||= document.parser
|
13
|
+
end
|
14
|
+
|
15
|
+
def handlers
|
16
|
+
find_parent = lambda do |parent_id|
|
17
|
+
parser.tree.find(parent_id)
|
18
|
+
end
|
19
|
+
|
20
|
+
get_manager = -> { self }
|
21
|
+
|
22
|
+
@handlers ||= publisher.order.inject({}) do |memo, level|
|
23
|
+
config = publisher.config_for_level(level)
|
24
|
+
klass_name = config.publish.klass
|
25
|
+
klass = klass_name.nil? ? Brief::Handlers::Base : (klass_name.to_s.constantize rescue Brief::Handlers::Base)
|
26
|
+
|
27
|
+
memo[level] = parser.tree.level(level).map do |element|
|
28
|
+
obj = klass.new(element)
|
29
|
+
|
30
|
+
obj.parent(&find_parent)
|
31
|
+
obj.get_manager(&get_manager)
|
32
|
+
|
33
|
+
obj
|
34
|
+
end
|
35
|
+
|
36
|
+
memo
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def run
|
41
|
+
publisher.order.each do |level|
|
42
|
+
objects = handlers[level]
|
43
|
+
objects.each(&:handle!)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Brief
|
2
|
+
class Publisher
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_accessor :defined, :aliases
|
6
|
+
end
|
7
|
+
|
8
|
+
self.defined ||= {}
|
9
|
+
self.aliases ||= {}
|
10
|
+
|
11
|
+
def self.find query
|
12
|
+
query = query.join(" ") if query.respond_to?(:to_ary)
|
13
|
+
|
14
|
+
if defined[query].nil? && aliases.has_key?(query)
|
15
|
+
return find(aliases[query])
|
16
|
+
end
|
17
|
+
|
18
|
+
result = defined[query]
|
19
|
+
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.publisher publisher_name
|
24
|
+
defined[key]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.define publisher_name, &config
|
28
|
+
obj = defined[publisher_name] ||= Brief::Publisher.new(publisher_name)
|
29
|
+
obj.instance_eval(&config) if block_given?
|
30
|
+
|
31
|
+
aliases[publisher_name.downcase] ||= obj.name
|
32
|
+
|
33
|
+
obj.aliases.each do |alias_name|
|
34
|
+
aliases[alias_name.to_s] ||= obj.name
|
35
|
+
end
|
36
|
+
|
37
|
+
obj
|
38
|
+
end
|
39
|
+
|
40
|
+
def process document
|
41
|
+
@manager = Brief::Publisher::HandlerManager.new(self, document).run
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_accessor :command_alias, :sample, :name, :options
|
45
|
+
|
46
|
+
def initialize(name, options={})
|
47
|
+
@name = name
|
48
|
+
@options = options
|
49
|
+
@command_alias = options[:command_alias]
|
50
|
+
|
51
|
+
@before_blocks = {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def before event_name, &block
|
55
|
+
(@before_blocks[event_name.to_sym] ||= []).push(block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_before_hooks event_name, &success
|
59
|
+
results = Array(@before_blocks[event_name.to_sym]).map do |hook|
|
60
|
+
hook.call if hook.respond_to?(:call)
|
61
|
+
end
|
62
|
+
|
63
|
+
unless results.any? {|r| r == false }
|
64
|
+
instance_eval(&success) if block_given?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def required_inputs
|
69
|
+
@options[:required_inputs] ||= {}
|
70
|
+
end
|
71
|
+
|
72
|
+
def requires_input field, options={}
|
73
|
+
cfg = required_inputs.fetch(field.to_sym) { options }
|
74
|
+
cfg.reverse_merge(options)
|
75
|
+
end
|
76
|
+
|
77
|
+
def levels &block
|
78
|
+
@state = :levels
|
79
|
+
instance_eval(&block) if block_given?
|
80
|
+
end
|
81
|
+
|
82
|
+
def sample sample_markdown=nil
|
83
|
+
options[:sample] = "#{sample_markdown}".gsub(/^\s+/,'') if sample_markdown
|
84
|
+
options[:sample]
|
85
|
+
end
|
86
|
+
|
87
|
+
def aliases *values
|
88
|
+
@command_aliases ||= []
|
89
|
+
@command_aliases += values
|
90
|
+
@command_aliases.uniq
|
91
|
+
end
|
92
|
+
|
93
|
+
def maximum_level
|
94
|
+
@maximum_level
|
95
|
+
end
|
96
|
+
|
97
|
+
def current_level
|
98
|
+
@current_level || 1
|
99
|
+
end
|
100
|
+
|
101
|
+
def order *levels
|
102
|
+
@order = levels if levels && levels.length > 0
|
103
|
+
@order || maximum_level.times.map {|n| n + 1 }
|
104
|
+
end
|
105
|
+
|
106
|
+
def level level_number, &block
|
107
|
+
@state = :level
|
108
|
+
@maximum_level = [@current_level || 0, level_number].compact.map(&:to_i).max
|
109
|
+
@current_level = level_number
|
110
|
+
instance_eval(&block) if block_given?
|
111
|
+
end
|
112
|
+
|
113
|
+
def level_config
|
114
|
+
@level_config ||= Hashie::Mash.new({})
|
115
|
+
end
|
116
|
+
|
117
|
+
def config_for_level level
|
118
|
+
level_config["level_#{level}".to_sym] ||= {}
|
119
|
+
end
|
120
|
+
|
121
|
+
def current_level_config
|
122
|
+
config_for_level(current_level)
|
123
|
+
end
|
124
|
+
|
125
|
+
def desc description
|
126
|
+
current_level_config[:description] = description
|
127
|
+
end
|
128
|
+
|
129
|
+
def define_handler handler, options={}
|
130
|
+
if options.is_a?(String)
|
131
|
+
options = {klass: options.classify}
|
132
|
+
end
|
133
|
+
|
134
|
+
current_level_config[handler] = options
|
135
|
+
end
|
136
|
+
|
137
|
+
def replaces_items_from_level n, options={}
|
138
|
+
current_level_config[:replace_options] = options
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
data/lib/brief/tree.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Brief
|
2
|
+
class Tree
|
3
|
+
def initialize(nodes)
|
4
|
+
@nodes = nodes
|
5
|
+
end
|
6
|
+
|
7
|
+
def find element_by_id
|
8
|
+
elements.detect {|el| el.id == element_by_id }
|
9
|
+
end
|
10
|
+
|
11
|
+
def elements
|
12
|
+
@elements ||= begin
|
13
|
+
@nodes.map do |node|
|
14
|
+
only = node.except(:children)
|
15
|
+
|
16
|
+
visitor = lambda do |child|
|
17
|
+
c_only = child.except(:children)
|
18
|
+
g_children = Array(child[:children])
|
19
|
+
g_children = nil if g_children.length == 0
|
20
|
+
|
21
|
+
g_children.each {|gchild| gchild.parent_id = c_only.id}
|
22
|
+
|
23
|
+
[c_only, g_children].compact
|
24
|
+
end
|
25
|
+
|
26
|
+
children = Array(node[:children]).map(&visitor).flatten
|
27
|
+
children.each {|child| child.parent_id ||= only.id }
|
28
|
+
|
29
|
+
children = nil if children.length == 0
|
30
|
+
|
31
|
+
[only, children].compact
|
32
|
+
end
|
33
|
+
|
34
|
+
end.flatten
|
35
|
+
end
|
36
|
+
|
37
|
+
def level level=1
|
38
|
+
elements.select {|el| el.level == level }
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/lib/brief.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
lib = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
module Brief
|
5
|
+
# Haven't decided if the brief config system should support different profiles or not
|
6
|
+
def self.profile
|
7
|
+
configuration
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.config
|
11
|
+
configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configuration
|
15
|
+
@configuration ||= Brief::Configuration.instance
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.root
|
19
|
+
Pathname(Dir.pwd())
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.gem_root
|
23
|
+
Pathname(File.dirname(__FILE__))
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.define publisher_name, &config
|
27
|
+
Brief::Publisher.define(publisher_name, &config)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'pathname'
|
32
|
+
require 'hashie'
|
33
|
+
require 'digest'
|
34
|
+
require 'yaml'
|
35
|
+
|
36
|
+
require 'active_support'
|
37
|
+
require 'active_support/core_ext'
|
38
|
+
|
39
|
+
require 'brief/line'
|
40
|
+
require 'brief/parser'
|
41
|
+
require 'brief/document'
|
42
|
+
require 'brief/tree'
|
43
|
+
require 'brief/version'
|
44
|
+
require 'brief/configuration'
|
45
|
+
|
46
|
+
require 'brief/publisher'
|
47
|
+
require 'brief/publisher/handler_manager'
|
48
|
+
# These should be able to be loaded separately
|
49
|
+
# some other way, but to help develoment..
|
50
|
+
require 'brief/handlers/base'
|
51
|
+
require 'brief/formatters/base'
|
52
|
+
|
53
|
+
# these may be optional one day
|
54
|
+
require 'brief/github'
|
55
|
+
require 'brief/git'
|
56
|
+
require 'brief/github/wiki'
|
data/lib/core_ext.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
class Object
|
2
|
+
def present?
|
3
|
+
!nil?
|
4
|
+
end
|
5
|
+
|
6
|
+
def try(*a, &b)
|
7
|
+
if a.empty? && block_given?
|
8
|
+
yield self
|
9
|
+
else
|
10
|
+
public_send(*a, &b) if respond_to?(a.first)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def try!(*a, &b)
|
15
|
+
if a.empty? && block_given?
|
16
|
+
yield self
|
17
|
+
else
|
18
|
+
public_send(*a, &b)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class NilClass
|
24
|
+
def present?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
def try(*args)
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def try!(*args)
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'core_ext/hash'
|
37
|
+
require 'core_ext/string'
|