saga 0.11.1 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE +1 -1
- data/README.rdoc +3 -8
- data/lib/saga.rb +2 -7
- data/lib/saga/document.rb +22 -24
- data/lib/saga/formatter.rb +51 -23
- data/lib/saga/parser.rb +15 -14
- data/lib/saga/planning.rb +37 -34
- data/lib/saga/runner.rb +60 -46
- data/lib/saga/tokenizer.rb +31 -32
- data/lib/saga/version.rb +5 -0
- metadata +19 -54
- data/Rakefile +0 -18
- data/VERSION +0 -1
- data/bin/saga +0 -5
- data/templates/default/document.erb +0 -97
- data/templates/default/helpers.rb +0 -32
- data/templates/requirements.txt.erb +0 -7
- data/templates/saga/document.erb +0 -34
- data/templates/saga/helpers.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 79f90ac99939d798864d05b8925eea44ca51ff11d38f33e74a07c7baa3a69c0c
|
4
|
+
data.tar.gz: 7e1f6ad4ac27d39b0f2b74a83c291ea8cb93f99099f95d1acf6f38d2d41dffb4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bda299077b1d0117ff16e927ad167fd6dc104e4e74a65870a173de96fa42b4b284b49ac6496174f24aebc4b20ec3254f4f5a41ea372a56a0b6c92449bd4b1e63
|
7
|
+
data.tar.gz: fc8e568db9e467b5fe47f250f11c4d78cdcffbd444947f33245632ec1a2cad4d3825565e4faaf4c373caf693b9b2e9e2402d935704d273576fde52e9a1683f3c
|
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c)
|
1
|
+
Copyright (c) Fingertips, Manfred Stienstra <manfred@fngtps.com>
|
2
2
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining
|
4
4
|
a copy of this software and associated documentation files (the
|
data/README.rdoc
CHANGED
@@ -7,7 +7,6 @@ and definitions. The document is usually a succinct and complete description of
|
|
7
7
|
a piece of software. Anyone reading the document should get a fairly good idea
|
8
8
|
of the application without further information.
|
9
9
|
|
10
|
-
|
11
10
|
=== The introduction
|
12
11
|
|
13
12
|
The first line of the document is the title. You can start the line with
|
@@ -25,7 +24,6 @@ The final part of the introduction is a concise description of the project.
|
|
25
24
|
|
26
25
|
Saga is a tool to convert the requirements format used at Fingertips to HTML.
|
27
26
|
|
28
|
-
|
29
27
|
=== The stories
|
30
28
|
|
31
29
|
The stories section starts with the USER STORIES header.
|
@@ -51,7 +49,6 @@ little text label at the end describes the status of the story.
|
|
51
49
|
A TextMate bundle for the stories format is available from:
|
52
50
|
https://github.com/Fingertips/stories.tmbundle
|
53
51
|
|
54
|
-
|
55
52
|
=== Definitions
|
56
53
|
|
57
54
|
The document ends with a list of definitions. Here we define words used
|
@@ -60,14 +57,13 @@ or if they might be misunderstood by someone. Like with the stories sections
|
|
60
57
|
are optional.
|
61
58
|
|
62
59
|
ROLES
|
63
|
-
|
60
|
+
|
64
61
|
Writer: Someone who was appointed the task of writing the stories.
|
65
62
|
Developer: A person developing the application.
|
66
|
-
|
63
|
+
|
67
64
|
DEFINITIONS
|
68
|
-
|
69
|
-
Project: The software project the developers are working on.
|
70
65
|
|
66
|
+
Project: The software project the developers are working on.
|
71
67
|
|
72
68
|
=== Template
|
73
69
|
|
@@ -82,7 +78,6 @@ requirements with the <tt>--template</tt> option:
|
|
82
78
|
|
83
79
|
$ saga convert --template design/requirements_template requirements.txt > requirements.html
|
84
80
|
|
85
|
-
|
86
81
|
=== Usage
|
87
82
|
|
88
83
|
Usage: saga [command]
|
data/lib/saga.rb
CHANGED
@@ -1,8 +1,3 @@
|
|
1
|
-
begin
|
2
|
-
require 'rubygems'
|
3
|
-
rescue LoadError
|
4
|
-
end
|
5
|
-
|
6
1
|
module Saga
|
7
2
|
autoload :Document, 'saga/document'
|
8
3
|
autoload :Formatter, 'saga/formatter'
|
@@ -10,10 +5,10 @@ module Saga
|
|
10
5
|
autoload :Planning, 'saga/planning'
|
11
6
|
autoload :Runner, 'saga/runner'
|
12
7
|
autoload :Tokenizer, 'saga/tokenizer'
|
13
|
-
|
8
|
+
|
14
9
|
def self.run(argv)
|
15
10
|
runner = ::Saga::Runner.new(argv)
|
16
11
|
runner.run
|
17
12
|
runner
|
18
13
|
end
|
19
|
-
end
|
14
|
+
end
|
data/lib/saga/document.rb
CHANGED
@@ -1,24 +1,22 @@
|
|
1
|
-
require 'active_support/ordered_hash'
|
2
|
-
|
3
1
|
module Saga
|
4
2
|
class Document
|
5
3
|
attr_accessor :title, :introduction, :authors, :stories, :definitions
|
6
|
-
|
4
|
+
|
7
5
|
def initialize
|
8
6
|
@title = ''
|
9
7
|
@introduction = []
|
10
8
|
@authors = []
|
11
|
-
@stories =
|
12
|
-
@definitions =
|
9
|
+
@stories = {}
|
10
|
+
@definitions = {}
|
13
11
|
end
|
14
|
-
|
12
|
+
|
15
13
|
def copy_story(story)
|
16
14
|
copied = {}
|
17
|
-
[
|
15
|
+
%i[id iteration status estimate description].each do |attribute|
|
18
16
|
copied[attribute] = story[attribute] if story[attribute]
|
19
17
|
end; copied
|
20
18
|
end
|
21
|
-
|
19
|
+
|
22
20
|
def flatten_stories(stories)
|
23
21
|
stories_as_flat_list = []
|
24
22
|
stories.flatten.each do |story|
|
@@ -30,26 +28,28 @@ module Saga
|
|
30
28
|
end
|
31
29
|
end; stories_as_flat_list
|
32
30
|
end
|
33
|
-
|
31
|
+
|
34
32
|
def stories_as_flat_list
|
35
33
|
flatten_stories(stories.values)
|
36
34
|
end
|
37
|
-
|
35
|
+
|
38
36
|
def _binding
|
39
37
|
binding
|
40
38
|
end
|
41
|
-
|
39
|
+
|
42
40
|
def used_ids
|
43
|
-
@stories.values.
|
41
|
+
@stories.values.each_with_object([]) do |stories, ids|
|
44
42
|
stories.each do |story|
|
45
43
|
ids << story[:id]
|
44
|
+
next unless story[:stories]
|
45
|
+
|
46
46
|
story[:stories].each do |nested|
|
47
47
|
ids << nested[:id]
|
48
|
-
end
|
49
|
-
end
|
48
|
+
end
|
49
|
+
end
|
50
50
|
end.compact
|
51
51
|
end
|
52
|
-
|
52
|
+
|
53
53
|
def unused_ids(limit)
|
54
54
|
position = 1
|
55
55
|
used_ids = used_ids()
|
@@ -59,29 +59,27 @@ module Saga
|
|
59
59
|
position
|
60
60
|
end
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
def length
|
64
64
|
stories_as_flat_list.length
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
def empty?
|
68
68
|
length == 0
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def _autofill_ids(stories, unused_ids)
|
72
72
|
stories.each do |story|
|
73
73
|
story[:id] ||= unused_ids.shift
|
74
|
-
if story[:stories]
|
75
|
-
_autofill_ids(story[:stories], unused_ids)
|
76
|
-
end
|
74
|
+
_autofill_ids(story[:stories], unused_ids) if story[:stories]
|
77
75
|
end
|
78
76
|
end
|
79
|
-
|
77
|
+
|
80
78
|
def autofill_ids
|
81
79
|
unused_ids = unused_ids(length - used_ids.length)
|
82
|
-
stories.each do |
|
80
|
+
stories.each do |_section, data|
|
83
81
|
_autofill_ids(data, unused_ids)
|
84
82
|
end
|
85
83
|
end
|
86
84
|
end
|
87
|
-
end
|
85
|
+
end
|
data/lib/saga/formatter.rb
CHANGED
@@ -1,42 +1,70 @@
|
|
1
|
-
require '
|
1
|
+
require 'erb'
|
2
2
|
|
3
3
|
module Saga
|
4
4
|
class Formatter
|
5
|
-
TEMPLATE_PATH = File.expand_path('
|
6
|
-
|
7
|
-
|
5
|
+
TEMPLATE_PATH = File.expand_path('../../templates', __dir__)
|
6
|
+
|
7
|
+
attr_reader :document
|
8
|
+
attr_reader :template_path
|
9
|
+
|
10
|
+
def initialize(document, template_path: nil)
|
8
11
|
@document = document
|
9
|
-
@
|
10
|
-
|
12
|
+
@template_path ||= template_path || File.join(self.class.template_path, 'default')
|
13
|
+
end
|
14
|
+
|
15
|
+
def template
|
16
|
+
@template ||= build_template
|
11
17
|
end
|
12
|
-
|
18
|
+
|
13
19
|
def format
|
14
|
-
|
20
|
+
@document.extend(ERB::Util) unless @document.is_a?(ERB::Util)
|
21
|
+
|
15
22
|
if File.exist?(helpers_file)
|
16
|
-
|
17
|
-
@document.extend(Helpers)
|
18
|
-
end
|
19
|
-
|
20
|
-
template_file = File.join(@options[:template], 'document.erb')
|
21
|
-
if File.exist?(template_file)
|
22
|
-
template = Erubis::Eruby.new(File.read(template_file))
|
23
|
-
template.result(@document._binding)
|
24
|
-
else
|
25
|
-
raise ArgumentError, "The template at path `#{template_file}' could not be found."
|
23
|
+
@document.instance_eval(File.read(helpers_file))
|
26
24
|
end
|
25
|
+
|
26
|
+
template.result(@document._binding)
|
27
27
|
end
|
28
|
-
|
29
|
-
def self.format(document,
|
30
|
-
formatter = new(document,
|
28
|
+
|
29
|
+
def self.format(document, **kwargs)
|
30
|
+
formatter = new(document, **kwargs)
|
31
31
|
formatter.format
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
def self.template_path
|
35
35
|
TEMPLATE_PATH
|
36
36
|
end
|
37
37
|
|
38
38
|
def self.saga_format(document)
|
39
|
-
format(document, :
|
39
|
+
format(document, template_path: File.join(template_path, 'saga'))
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
if RUBY_VERSION < '2.6.0'
|
45
|
+
def build_erb
|
46
|
+
ERB.new(File.read(template_file), nil, '-')
|
47
|
+
end
|
48
|
+
else
|
49
|
+
def build_erb
|
50
|
+
ERB.new(File.read(template_file), trim_mode: '-')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_template
|
55
|
+
if File.exist?(template_file)
|
56
|
+
build_erb
|
57
|
+
else
|
58
|
+
raise ArgumentError, "The template at path `#{template_file}' could not be found."
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def helpers_file
|
63
|
+
File.join(template_path, 'helpers.rb')
|
64
|
+
end
|
65
|
+
|
66
|
+
def template_file
|
67
|
+
File.join(template_path, 'document.erb')
|
40
68
|
end
|
41
69
|
end
|
42
70
|
end
|
data/lib/saga/parser.rb
CHANGED
@@ -1,41 +1,41 @@
|
|
1
1
|
module Saga
|
2
2
|
class Parser
|
3
3
|
attr_accessor :document
|
4
|
-
|
4
|
+
|
5
5
|
def initialize
|
6
6
|
@tokenizer = ::Saga::Tokenizer.new(self)
|
7
7
|
@document = ::Saga::Document.new
|
8
8
|
self.current_section = :title
|
9
9
|
@current_header = ''
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def current_section=(section)
|
13
13
|
@current_section = section
|
14
14
|
@tokenizer.current_section = section
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def parse(input)
|
18
18
|
@tokenizer.process(input)
|
19
19
|
@document
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def handle_author(author)
|
23
23
|
@document.authors << author
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
def handle_story(story)
|
27
27
|
self.current_section = :stories
|
28
28
|
@document.stories[@current_header] ||= []
|
29
29
|
@document.stories[@current_header] << story
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
def handle_nested_story(story)
|
33
33
|
self.current_section = :story
|
34
34
|
parent = @document.stories[@current_header][-1]
|
35
35
|
parent[:stories] ||= []
|
36
36
|
parent[:stories] << story
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
def handle_notes(notes)
|
40
40
|
story = @document.stories[@current_header][-1]
|
41
41
|
if @current_section == :story
|
@@ -44,33 +44,34 @@ module Saga
|
|
44
44
|
story[:notes] = notes
|
45
45
|
end
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
def handle_definition(definition)
|
49
49
|
self.current_section = :definitions
|
50
50
|
@document.definitions[@current_header] ||= []
|
51
51
|
@document.definitions[@current_header] << definition
|
52
52
|
end
|
53
|
-
|
53
|
+
|
54
54
|
def handle_string(string)
|
55
55
|
return if string.strip == ''
|
56
|
+
|
56
57
|
if string.strip == 'USER STORIES'
|
57
58
|
self.current_section = :stories
|
58
59
|
return @current_section
|
59
60
|
end
|
60
|
-
|
61
|
-
if
|
61
|
+
|
62
|
+
if @current_section == :title
|
62
63
|
@document.title = string.gsub(/^requirements/i, '').strip
|
63
64
|
self.current_section = :introduction
|
64
|
-
elsif
|
65
|
+
elsif @current_section == :introduction
|
65
66
|
@document.introduction << string
|
66
67
|
else
|
67
68
|
@current_header = string.strip
|
68
69
|
end
|
69
70
|
end
|
70
|
-
|
71
|
+
|
71
72
|
def self.parse(input)
|
72
73
|
parser = new
|
73
74
|
parser.parse(input)
|
74
75
|
end
|
75
76
|
end
|
76
|
-
end
|
77
|
+
end
|
data/lib/saga/planning.rb
CHANGED
@@ -1,32 +1,35 @@
|
|
1
1
|
module Saga
|
2
2
|
class Planning
|
3
|
-
BLANK_ITERATION = {:
|
4
|
-
|
3
|
+
BLANK_ITERATION = { story_count: 0, estimate_total_in_hours: 0 }.freeze
|
4
|
+
|
5
5
|
def initialize(document)
|
6
|
+
unless document
|
7
|
+
raise ArgumentError, 'Please supply a document for planning.'
|
8
|
+
end
|
9
|
+
|
6
10
|
@document = document
|
7
11
|
end
|
8
|
-
|
12
|
+
|
9
13
|
def iterations
|
10
|
-
@document.stories_as_flat_list.
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
properties
|
14
|
+
@document.stories_as_flat_list.each_with_object({}) do |story, properties|
|
15
|
+
next unless story[:estimate]
|
16
|
+
|
17
|
+
iteration = story[:iteration] || -1
|
18
|
+
properties[iteration] ||= BLANK_ITERATION.dup
|
19
|
+
properties[iteration][:story_count] += 1
|
20
|
+
properties[iteration][:estimate_total_in_hours] += self.class.estimate_to_hours(story[:estimate])
|
18
21
|
end
|
19
22
|
end
|
20
|
-
|
23
|
+
|
21
24
|
def total
|
22
25
|
total = BLANK_ITERATION.dup
|
23
|
-
iterations.each do |
|
26
|
+
iterations.each do |_iteration, properties|
|
24
27
|
total[:story_count] += properties[:story_count]
|
25
28
|
total[:estimate_total_in_hours] += properties[:estimate_total_in_hours]
|
26
29
|
end
|
27
30
|
total
|
28
31
|
end
|
29
|
-
|
32
|
+
|
30
33
|
def unestimated
|
31
34
|
unestimated = 0
|
32
35
|
@document.stories_as_flat_list.each do |story|
|
@@ -34,7 +37,7 @@ module Saga
|
|
34
37
|
end
|
35
38
|
unestimated
|
36
39
|
end
|
37
|
-
|
40
|
+
|
38
41
|
def range_estimated
|
39
42
|
range_estimated = 0
|
40
43
|
@document.stories_as_flat_list.each do |story|
|
@@ -44,31 +47,31 @@ module Saga
|
|
44
47
|
end
|
45
48
|
range_estimated
|
46
49
|
end
|
47
|
-
|
50
|
+
|
48
51
|
def statusses
|
49
52
|
statusses = {}
|
50
53
|
@document.stories_as_flat_list.each do |story|
|
51
|
-
if story[:estimate]
|
54
|
+
if story[:estimate] && story[:status]
|
52
55
|
statusses[story[:status]] ||= 0
|
53
56
|
statusses[story[:status]] += self.class.estimate_to_hours(story[:estimate])
|
54
57
|
end
|
55
58
|
end
|
56
59
|
statusses
|
57
60
|
end
|
58
|
-
|
61
|
+
|
59
62
|
def to_s
|
60
63
|
if @document.empty?
|
61
|
-
|
64
|
+
'There are no stories yet.'
|
62
65
|
else
|
63
66
|
parts = iterations.keys.sort.map do |iteration|
|
64
67
|
self.class.format_properties(iteration, iterations[iteration])
|
65
68
|
end
|
66
69
|
unless parts.empty?
|
67
70
|
formatted_totals = self.class.format_properties(false, total)
|
68
|
-
parts << '-'*formatted_totals.length
|
71
|
+
parts << '-' * formatted_totals.length
|
69
72
|
parts << formatted_totals
|
70
73
|
end
|
71
|
-
if unestimated > 0
|
74
|
+
if (unestimated > 0) || !statusses.empty?
|
72
75
|
parts << ''
|
73
76
|
parts << self.class.format_unestimated(unestimated) if unestimated > 0
|
74
77
|
parts << self.class.format_range_estimated(range_estimated) if range_estimated > 0
|
@@ -78,9 +81,9 @@ module Saga
|
|
78
81
|
parts.join("\n")
|
79
82
|
end
|
80
83
|
end
|
81
|
-
|
84
|
+
|
82
85
|
FIRST_COLUMN_WIDTH = 14
|
83
|
-
|
86
|
+
|
84
87
|
def self.estimate_to_hours(estimate)
|
85
88
|
case estimate[1]
|
86
89
|
when :days
|
@@ -93,29 +96,29 @@ module Saga
|
|
93
96
|
estimate[0]
|
94
97
|
end
|
95
98
|
end
|
96
|
-
|
99
|
+
|
97
100
|
def self.format_properties(iteration, properties)
|
98
|
-
if iteration
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
101
|
+
label = if iteration
|
102
|
+
iteration == -1 ? 'Unplanned' : "Iteration #{iteration}"
|
103
|
+
else
|
104
|
+
'Total'
|
105
|
+
end
|
103
106
|
story_column = format_stories_count(properties[:story_count])
|
104
107
|
"#{label.ljust(FIRST_COLUMN_WIDTH)}: #{properties[:estimate_total_in_hours]} (#{story_column})"
|
105
108
|
end
|
106
|
-
|
109
|
+
|
107
110
|
def self.format_unestimated(unestimated)
|
108
111
|
"Unestimated : #{format_stories_count(unestimated)}"
|
109
112
|
end
|
110
|
-
|
113
|
+
|
111
114
|
def self.format_range_estimated(range_estimated)
|
112
115
|
"Range-estimate: #{format_stories_count(range_estimated)}"
|
113
116
|
end
|
114
|
-
|
117
|
+
|
115
118
|
def self.format_stories_count(count)
|
116
119
|
count > 1 ? "#{count} stories" : 'one story'
|
117
120
|
end
|
118
|
-
|
121
|
+
|
119
122
|
def self.format_statusses(statusses)
|
120
123
|
parts = []
|
121
124
|
statusses.each do |status, hours|
|
@@ -124,4 +127,4 @@ module Saga
|
|
124
127
|
parts.join("\n")
|
125
128
|
end
|
126
129
|
end
|
127
|
-
end
|
130
|
+
end
|
data/lib/saga/runner.rb
CHANGED
@@ -1,56 +1,70 @@
|
|
1
1
|
require 'optparse'
|
2
|
-
|
2
|
+
require 'ostruct'
|
3
|
+
|
3
4
|
module Saga
|
4
5
|
class Runner
|
5
6
|
def initialize(argv)
|
6
7
|
@argv = argv
|
7
|
-
@options = {}
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
|
+
def options
|
11
|
+
unless defined?(@options)
|
12
|
+
@options = OpenStruct.new(run: true)
|
13
|
+
parser.parse!(@argv)
|
14
|
+
end
|
15
|
+
@options
|
16
|
+
end
|
17
|
+
|
10
18
|
def parser
|
11
|
-
@parser ||= OptionParser.new do |
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
@options
|
19
|
+
@parser ||= OptionParser.new do |parser|
|
20
|
+
parser.banner = 'Usage: saga [command]'
|
21
|
+
parser.separator ''
|
22
|
+
parser.separator 'Commands:'
|
23
|
+
parser.separator ' new - prints a blank stub'
|
24
|
+
parser.separator ' convert <filename> - convert the stories to HTML'
|
25
|
+
parser.separator ' inspect <filename> - print the internals of the document'
|
26
|
+
parser.separator ' autofill <filename> - adds an id to stories without one'
|
27
|
+
parser.separator ' planning <filename> - shows the planning of stories in iterations'
|
28
|
+
parser.separator ' template <dir> - creates a template directory'
|
29
|
+
parser.separator ''
|
30
|
+
parser.separator 'Options:'
|
31
|
+
parser.on('-t', '--template DIR', 'Use an external template for conversion to HTML') do |template_path|
|
32
|
+
@options.template_path = File.expand_path(template_path)
|
25
33
|
end
|
26
|
-
|
27
|
-
puts
|
28
|
-
|
34
|
+
parser.on('-h', '--help', 'Show help') do
|
35
|
+
puts parser
|
36
|
+
@options.run = false
|
29
37
|
end
|
30
38
|
end
|
31
39
|
end
|
32
|
-
|
40
|
+
|
33
41
|
def new_file
|
34
42
|
document = Saga::Document.new
|
35
43
|
document.title = 'Title'
|
36
|
-
document.authors <<
|
44
|
+
document.authors << author
|
37
45
|
document.stories[''] = [{
|
38
|
-
:
|
39
|
-
:
|
40
|
-
:
|
46
|
+
description: 'As a writer I would like to write stories so developers can implement them.',
|
47
|
+
id: 1,
|
48
|
+
status: 'todo'
|
41
49
|
}]
|
42
50
|
document.definitions[''] = [{
|
43
|
-
:
|
44
|
-
:
|
51
|
+
title: 'Writer',
|
52
|
+
definition: 'Someone who is responsible for writing down requirements in the form of stories'
|
45
53
|
}]
|
46
|
-
|
54
|
+
|
47
55
|
Saga::Formatter.saga_format(document)
|
48
56
|
end
|
49
|
-
|
50
|
-
def
|
51
|
-
|
57
|
+
|
58
|
+
def convert_options
|
59
|
+
{
|
60
|
+
template_path: options.template_path
|
61
|
+
}.compact
|
62
|
+
end
|
63
|
+
|
64
|
+
def convert(filename)
|
65
|
+
Saga::Formatter.format(Saga::Parser.parse(File.read(filename)), **convert_options)
|
52
66
|
end
|
53
|
-
|
67
|
+
|
54
68
|
def write_parsed_document(filename)
|
55
69
|
document = Saga::Parser.parse(File.read(filename))
|
56
70
|
puts document.title
|
@@ -60,17 +74,17 @@ module Saga
|
|
60
74
|
puts
|
61
75
|
document.definitions.each { |header, definitions| puts header; definitions.each { |definition| p definition } }
|
62
76
|
end
|
63
|
-
|
77
|
+
|
64
78
|
def autofill(filename)
|
65
79
|
document = Saga::Parser.parse(File.read(filename))
|
66
80
|
document.autofill_ids
|
67
81
|
Saga::Formatter.saga_format(document)
|
68
82
|
end
|
69
|
-
|
83
|
+
|
70
84
|
def planning(filename)
|
71
85
|
Saga::Planning.new(Saga::Parser.parse(File.read(filename))).to_s
|
72
86
|
end
|
73
|
-
|
87
|
+
|
74
88
|
def copy_template(destination)
|
75
89
|
if File.exist?(destination)
|
76
90
|
puts "The directory `#{destination}' already exists!"
|
@@ -81,13 +95,13 @@ module Saga
|
|
81
95
|
FileUtils.cp(File.join(Saga::Formatter.template_path, 'default/document.erb'), destination)
|
82
96
|
end
|
83
97
|
end
|
84
|
-
|
85
|
-
def run_command(command
|
98
|
+
|
99
|
+
def run_command(command)
|
86
100
|
case command
|
87
101
|
when 'new'
|
88
102
|
puts new_file
|
89
103
|
when 'convert'
|
90
|
-
puts convert(File.expand_path(@argv[0])
|
104
|
+
puts convert(File.expand_path(@argv[0]))
|
91
105
|
when 'inspect'
|
92
106
|
write_parsed_document(File.expand_path(@argv[0]))
|
93
107
|
when 'autofill'
|
@@ -97,22 +111,22 @@ module Saga
|
|
97
111
|
when 'template'
|
98
112
|
copy_template(File.expand_path(@argv[0]))
|
99
113
|
else
|
100
|
-
puts convert(File.expand_path(command)
|
114
|
+
puts convert(File.expand_path(command))
|
101
115
|
end
|
102
116
|
end
|
103
|
-
|
117
|
+
|
104
118
|
def run
|
105
|
-
|
119
|
+
return unless options.run
|
120
|
+
|
106
121
|
if command = @argv.shift
|
107
|
-
run_command(command
|
122
|
+
run_command(command)
|
108
123
|
else
|
109
124
|
puts parser.to_s
|
110
125
|
end
|
111
126
|
end
|
112
|
-
|
113
|
-
def
|
114
|
-
name
|
115
|
-
{:name => name}
|
127
|
+
|
128
|
+
def author
|
129
|
+
{ name: `osascript -e "long user name of (system info)" &1> /dev/null`.strip }
|
116
130
|
end
|
117
131
|
end
|
118
132
|
end
|
data/lib/saga/tokenizer.rb
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
module Saga
|
2
2
|
class Tokenizer
|
3
3
|
attr_accessor :current_section
|
4
|
-
|
5
|
-
RE_STORY =
|
6
|
-
RE_DEFINITION = /\A[[:alpha:]]([[:alpha:]]|[\s-])
|
7
|
-
|
4
|
+
|
5
|
+
RE_STORY = /\./.freeze
|
6
|
+
RE_DEFINITION = /\A[[:alpha:]]([[:alpha:]]|[\s-])+:/.freeze
|
7
|
+
|
8
8
|
def initialize(parser)
|
9
9
|
@parser = parser
|
10
|
-
@
|
10
|
+
@current_section = nil
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
def expect_stories?
|
14
|
-
%w
|
14
|
+
%w[story stories].include?(current_section.to_s)
|
15
15
|
end
|
16
|
-
|
17
|
-
def process_line(input, index=0)
|
18
|
-
if input[0,2] == ' '
|
16
|
+
|
17
|
+
def process_line(input, index = 0)
|
18
|
+
if input[0, 2] == ' '
|
19
19
|
@parser.handle_notes(input.strip)
|
20
|
-
elsif input[0,3] == '| '
|
20
|
+
elsif input[0, 3] == '| '
|
21
21
|
@parser.handle_notes(input[1..-1].strip)
|
22
|
-
elsif input[0,1] == '|'
|
22
|
+
elsif input[0, 1] == '|'
|
23
23
|
@parser.handle_nested_story(self.class.tokenize_story(input[1..-1]))
|
24
|
-
elsif input[0,1] == '-'
|
24
|
+
elsif input[0, 1] == '-'
|
25
25
|
@parser.handle_author(self.class.tokenize_author(input))
|
26
26
|
elsif input =~ RE_DEFINITION
|
27
27
|
@parser.handle_definition(self.class.tokenize_definition(input))
|
@@ -30,17 +30,17 @@ module Saga
|
|
30
30
|
else
|
31
31
|
@parser.handle_string(input)
|
32
32
|
end
|
33
|
-
rescue
|
33
|
+
rescue StandardError
|
34
34
|
$stderr.write "On line #{index}: #{input.inspect}:"
|
35
35
|
raise
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def process(input)
|
39
39
|
input.split("\n").each_with_index do |line, index|
|
40
40
|
process_line(line, index)
|
41
41
|
end
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
def self.interval(input)
|
45
45
|
case input.strip
|
46
46
|
when 'd'
|
@@ -51,18 +51,18 @@ module Saga
|
|
51
51
|
:hours
|
52
52
|
end
|
53
53
|
end
|
54
|
-
|
55
|
-
RE_STORY_NUMBER = /\#(\d+)
|
56
|
-
RE_STORY_ITERATION = /i(\d+)
|
57
|
-
RE_STORY_ESTIMATE_PART = /(\d+)(d|w|h|)
|
58
|
-
|
54
|
+
|
55
|
+
RE_STORY_NUMBER = /\#(\d+)/.freeze
|
56
|
+
RE_STORY_ITERATION = /i(\d+)/.freeze
|
57
|
+
RE_STORY_ESTIMATE_PART = /(\d+)(d|w|h|)/.freeze
|
58
|
+
|
59
59
|
def self.tokenize_story_attributes(input)
|
60
60
|
return {} if input.nil?
|
61
|
-
|
61
|
+
|
62
62
|
attributes = {}
|
63
63
|
rest = []
|
64
64
|
parts = input.split(/\s/)
|
65
|
-
|
65
|
+
|
66
66
|
parts.each do |part|
|
67
67
|
if part.strip == ''
|
68
68
|
next
|
@@ -71,7 +71,7 @@ module Saga
|
|
71
71
|
elsif match = RE_STORY_ITERATION.match(part)
|
72
72
|
attributes[:iteration] = match[1].to_i
|
73
73
|
elsif match = /#{RE_STORY_ESTIMATE_PART}-#{RE_STORY_ESTIMATE_PART}/.match(part)
|
74
|
-
estimate = "#{match[1,2].join}-#{match[3,2].join}"
|
74
|
+
estimate = "#{match[1, 2].join}-#{match[3, 2].join}"
|
75
75
|
attributes[:estimate] = [estimate, :range]
|
76
76
|
elsif match = RE_STORY_ESTIMATE_PART.match(part)
|
77
77
|
attributes[:estimate] = [match[1].to_i, interval(match[2])]
|
@@ -79,31 +79,30 @@ module Saga
|
|
79
79
|
rest << part
|
80
80
|
end
|
81
81
|
end
|
82
|
-
|
82
|
+
|
83
83
|
attributes[:status] = rest.join(' ') unless rest.empty?
|
84
84
|
attributes
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
87
|
def self.tokenize_story(input)
|
88
|
-
lines = input.split('\n')
|
89
88
|
parts = input.split(' - ')
|
90
89
|
if parts.length > 1
|
91
90
|
story = tokenize_story_attributes(parts[-1])
|
92
91
|
story[:description] = parts[0..-2].join('-').strip
|
93
92
|
story
|
94
93
|
else
|
95
|
-
{ :
|
94
|
+
{ description: input.strip }
|
96
95
|
end
|
97
96
|
end
|
98
|
-
|
97
|
+
|
99
98
|
def self.tokenize_definition(input)
|
100
99
|
if match = /^([^:]+)\s*:\s*(.+)\s*$/.match(input)
|
101
|
-
{:
|
100
|
+
{ title: match[1], definition: match[2] }
|
102
101
|
else
|
103
102
|
{}
|
104
103
|
end
|
105
104
|
end
|
106
|
-
|
105
|
+
|
107
106
|
def self.tokenize_author(input)
|
108
107
|
author = {}
|
109
108
|
parts = input[1..-1].split(',')
|
@@ -114,4 +113,4 @@ module Saga
|
|
114
113
|
author
|
115
114
|
end
|
116
115
|
end
|
117
|
-
end
|
116
|
+
end
|
data/lib/saga/version.rb
ADDED
metadata
CHANGED
@@ -1,59 +1,31 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saga
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manfred Stienstra
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-04-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: erubis
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '2.6'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '2.6'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: activesupport
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
30
16
|
requirements:
|
31
|
-
- - "
|
17
|
+
- - "~>"
|
32
18
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
19
|
+
version: '5'
|
34
20
|
type: :runtime
|
35
21
|
prerelease: false
|
36
22
|
version_requirements: !ruby/object:Gem::Requirement
|
37
23
|
requirements:
|
38
|
-
- - "
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '2.3'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: bacon
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
24
|
+
- - "~>"
|
53
25
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
26
|
+
version: '5'
|
55
27
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
28
|
+
name: rake
|
57
29
|
requirement: !ruby/object:Gem::Requirement
|
58
30
|
requirements:
|
59
31
|
- - ">="
|
@@ -66,20 +38,17 @@ dependencies:
|
|
66
38
|
- - ">="
|
67
39
|
- !ruby/object:Gem::Version
|
68
40
|
version: '0'
|
69
|
-
description:
|
70
|
-
|
71
|
-
|
72
|
-
|
41
|
+
description: |2
|
42
|
+
Saga reads its own story format and formats to other output formats using
|
43
|
+
templates.
|
44
|
+
email:
|
45
|
+
- manfred@fngtps.com
|
46
|
+
executables: []
|
73
47
|
extensions: []
|
74
|
-
extra_rdoc_files:
|
75
|
-
- LICENSE
|
76
|
-
- README.rdoc
|
48
|
+
extra_rdoc_files: []
|
77
49
|
files:
|
78
50
|
- LICENSE
|
79
51
|
- README.rdoc
|
80
|
-
- Rakefile
|
81
|
-
- VERSION
|
82
|
-
- bin/saga
|
83
52
|
- lib/saga.rb
|
84
53
|
- lib/saga/document.rb
|
85
54
|
- lib/saga/formatter.rb
|
@@ -87,13 +56,10 @@ files:
|
|
87
56
|
- lib/saga/planning.rb
|
88
57
|
- lib/saga/runner.rb
|
89
58
|
- lib/saga/tokenizer.rb
|
90
|
-
-
|
91
|
-
|
92
|
-
|
93
|
-
-
|
94
|
-
- templates/saga/helpers.rb
|
95
|
-
homepage:
|
96
|
-
licenses: []
|
59
|
+
- lib/saga/version.rb
|
60
|
+
homepage: https://github.com/Fingertips/saga
|
61
|
+
licenses:
|
62
|
+
- MIT
|
97
63
|
metadata: {}
|
98
64
|
post_install_message:
|
99
65
|
rdoc_options: []
|
@@ -110,8 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
76
|
- !ruby/object:Gem::Version
|
111
77
|
version: '0'
|
112
78
|
requirements: []
|
113
|
-
|
114
|
-
rubygems_version: 2.2.2
|
79
|
+
rubygems_version: 3.0.3
|
115
80
|
signing_key:
|
116
81
|
specification_version: 4
|
117
82
|
summary: Saga is a tool to convert stories syntax to a nicely formatted document.
|
data/Rakefile
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
require 'rake'
|
2
|
-
require 'rdoc/task'
|
3
|
-
|
4
|
-
desc "Run all specs by default"
|
5
|
-
task :default => [:spec]
|
6
|
-
|
7
|
-
desc "Run all specs"
|
8
|
-
task :spec do
|
9
|
-
sh 'bacon test/*_spec.rb'
|
10
|
-
end
|
11
|
-
|
12
|
-
namespace :documentation do
|
13
|
-
Rake::RDocTask.new(:generate) do |rd|
|
14
|
-
rd.main = "README.rdoc"
|
15
|
-
rd.rdoc_files.include("README.rdoc", "LICENSE", "bin/**/*.rb", "lib/**/*.rb", "templates/**/*.rb")
|
16
|
-
rd.options << "--all" << "--charset" << "utf-8"
|
17
|
-
end
|
18
|
-
end
|
data/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.10.0
|
data/bin/saga
DELETED
@@ -1,97 +0,0 @@
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
2
|
-
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
3
|
-
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
4
|
-
<head>
|
5
|
-
<title>Requirements <%= title %> · Fingertips</title>
|
6
|
-
<link rel="stylesheet" type="text/css" media="all" href="http://resources.fngtps.com/2006/doc.css" />
|
7
|
-
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
8
|
-
<style type="text/css">
|
9
|
-
td.id { text-align: right !important; }
|
10
|
-
td.id a { color: #000; }
|
11
|
-
table.review { border-bottom: 1px solid #ccc; margin-top: -1em;}
|
12
|
-
table.review td { padding: .25em 0; vertical-align: top; text-align: left; min-width: 1em; }
|
13
|
-
table.review th { padding: .25em 0; vertical-align: top; text-align: right; }
|
14
|
-
table.review td.story { width: 100%; border-top: 1px solid #ccc; }
|
15
|
-
table.review td.meta { padding-left: 1em; border-top: 1px solid #ccc; text-align: right !important; white-space: nowrap; }
|
16
|
-
table.review td.notes { color: #666; padding: 0 0 .25em 0; font-style: italic; }
|
17
|
-
table.review .dropped { color: #666; text-decoration: line-through; }
|
18
|
-
table.review .done { color: #666; background-color: #f0f8ff; }
|
19
|
-
table.review tr.nested td.story { padding-left: 1em; }
|
20
|
-
</style>
|
21
|
-
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
|
22
|
-
</head>
|
23
|
-
<body id="doc" class="requirements">
|
24
|
-
|
25
|
-
<p id="logo"><img src="http://resources.fngtps.com/2006/print-logo.png" alt="Fingertips design & development" /> </p>
|
26
|
-
|
27
|
-
<h1>Requirements <br /><%= title %></h1>
|
28
|
-
|
29
|
-
<% authors.each do |author| %>
|
30
|
-
<p class="author"><%= format_author(author) %></p>
|
31
|
-
<% end %>
|
32
|
-
|
33
|
-
<% introduction.each do |paragraph| %>
|
34
|
-
<p><%== paragraph %></p>
|
35
|
-
<% end %>
|
36
|
-
|
37
|
-
<% unless stories.empty? %>
|
38
|
-
<h2>User stories</h2>
|
39
|
-
<% stories.each do |header, stories| %>
|
40
|
-
<% unless header.strip == '' %>
|
41
|
-
<h3><%= header %></h3>
|
42
|
-
<% end %>
|
43
|
-
<table class="review">
|
44
|
-
<tr>
|
45
|
-
<th></th>
|
46
|
-
<th title="id">#</th>
|
47
|
-
<th title="Estimate">e</th>
|
48
|
-
<th title="Iteration">i</th>
|
49
|
-
<th title="Status">s</th>
|
50
|
-
</tr>
|
51
|
-
<% stories.each do |story| %>
|
52
|
-
<tr class="<%= story[:status] %>" id="story<%= story[:id] %>">
|
53
|
-
<td class="story"><%= story[:description] %></td>
|
54
|
-
<td class="meta id"><%= story[:id] %></td>
|
55
|
-
<td class="meta estimate"><%= format_estimate(*story[:estimate]) if story[:estimate] %></td>
|
56
|
-
<td class="meta iteration"><%= story[:iteration] %></td>
|
57
|
-
<td class="meta status"><%= story[:status] %></td>
|
58
|
-
</tr>
|
59
|
-
<% if story[:notes] %>
|
60
|
-
<tr class="<%= story[:status] %>">
|
61
|
-
<td class="notes" colspan="5"><%= story[:notes] %></td>
|
62
|
-
</tr>
|
63
|
-
<% end %>
|
64
|
-
<% if story[:stories] %>
|
65
|
-
<% story[:stories].each do |nested| %>
|
66
|
-
<tr class="nested <%= nested[:status] %>" id="story<%= nested[:id] %>">
|
67
|
-
<td class="story"><%= nested[:description] %></td>
|
68
|
-
<td class="meta id"><%= nested[:id] %></td>
|
69
|
-
<td class="meta estimate"><%= format_estimate(*nested[:estimate]) if nested[:estimate] %></td>
|
70
|
-
<td class="meta iteration"><%= nested[:iteration] %></td>
|
71
|
-
<td class="meta status"><%= nested[:status] %></td>
|
72
|
-
</tr>
|
73
|
-
<% if nested[:notes] %>
|
74
|
-
<tr class="nested <%= nested[:status] %>">
|
75
|
-
<td class="notes" colspan="5"><%= nested[:notes] %></td>
|
76
|
-
</tr>
|
77
|
-
<% end %>
|
78
|
-
<% end %>
|
79
|
-
<% end %>
|
80
|
-
<% end %>
|
81
|
-
</table>
|
82
|
-
<% end %>
|
83
|
-
<% end %>
|
84
|
-
|
85
|
-
<% definitions.each do |header, definitions| %>
|
86
|
-
<% unless header.strip == '' %>
|
87
|
-
<h2><%= format_header(header) %></h2>
|
88
|
-
<% end %>
|
89
|
-
<dl>
|
90
|
-
<% definitions.each do |definition| %>
|
91
|
-
<dt><%= definition[:title] %></dt>
|
92
|
-
<dd><%= definition[:definition] %></dd>
|
93
|
-
<% end %>
|
94
|
-
</dl>
|
95
|
-
<% end %>
|
96
|
-
</body>
|
97
|
-
</html>
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module Helpers
|
2
|
-
def format_author(author)
|
3
|
-
parts = []
|
4
|
-
parts << author[:name] if author[:name]
|
5
|
-
parts << "<a href=\"mailto:#{author[:email]}\">#{author[:email]}</a>" if author[:email]
|
6
|
-
if author[:website] and author[:company]
|
7
|
-
parts << "<a href=\"#{author[:website]}\">#{author[:company]}</a>"
|
8
|
-
elsif author[:company]
|
9
|
-
parts << author[:company]
|
10
|
-
end
|
11
|
-
parts.join(', ')
|
12
|
-
end
|
13
|
-
|
14
|
-
def format_header(header)
|
15
|
-
"#{header[0,1].upcase}#{header[1..-1].downcase}"
|
16
|
-
end
|
17
|
-
|
18
|
-
def pluralize(cardinality, singular, plural)
|
19
|
-
[cardinality, cardinality == 1 ? singular : plural].join(' ')
|
20
|
-
end
|
21
|
-
|
22
|
-
def format_estimate(cardinality, interval)
|
23
|
-
case interval
|
24
|
-
when :days
|
25
|
-
pluralize(cardinality, 'day', 'days')
|
26
|
-
when :weeks
|
27
|
-
pluralize(cardinality, 'week', 'days')
|
28
|
-
else
|
29
|
-
cardinality.to_s
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
data/templates/saga/document.erb
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
Requirements <%= title %>
|
2
|
-
<% unless authors.empty? %>
|
3
|
-
|
4
|
-
<% authors.each do |author| -%>
|
5
|
-
- <%= format_author(author) -%>
|
6
|
-
|
7
|
-
<% end -%>
|
8
|
-
<% end -%>
|
9
|
-
|
10
|
-
<% introduction.each do |paragraph| %>
|
11
|
-
<%= paragraph %>
|
12
|
-
|
13
|
-
<% end %>
|
14
|
-
USER STORIES
|
15
|
-
<% stories.each do |header, stories| -%>
|
16
|
-
<% if header.strip == '' %>
|
17
|
-
|
18
|
-
<% else %>
|
19
|
-
<%= "\n#{header}\n\n" -%>
|
20
|
-
<% end %>
|
21
|
-
<% stories.each do |story| -%>
|
22
|
-
<%= format_story(story) -%>
|
23
|
-
<% end -%>
|
24
|
-
<% end -%>
|
25
|
-
<% definitions.each do |header, definitions| -%>
|
26
|
-
<% if header.strip == '' %>
|
27
|
-
|
28
|
-
<% else %>
|
29
|
-
<%= "\n#{header}\n\n" -%>
|
30
|
-
<% end %>
|
31
|
-
<% definitions.each do |definition| -%>
|
32
|
-
<%= format_definition(definition) %>
|
33
|
-
<% end %>
|
34
|
-
<% end %>
|
data/templates/saga/helpers.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
module Helpers
|
2
|
-
def format_author(author)
|
3
|
-
[:name, :email, :company, :website].map do |key|
|
4
|
-
author[key]
|
5
|
-
end.compact.join(', ')
|
6
|
-
end
|
7
|
-
|
8
|
-
def format_estimate(cardinality, interval)
|
9
|
-
case interval
|
10
|
-
when :days
|
11
|
-
"#{cardinality}d"
|
12
|
-
when :weeks
|
13
|
-
"#{cardinality}w"
|
14
|
-
else
|
15
|
-
cardinality.to_s
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def format_story(story, kind=:regular)
|
20
|
-
story_attributes = []
|
21
|
-
story_attributes << "##{story[:id]}" if story[:id]
|
22
|
-
story_attributes << story[:status] if story[:status]
|
23
|
-
story_attributes << format_estimate(*story[:estimate]) if story[:estimate]
|
24
|
-
story_attributes << "i#{story[:iteration]}" if story[:iteration]
|
25
|
-
|
26
|
-
prefix = (kind == :nested) ? '| ' : ''
|
27
|
-
formatted = "#{prefix}#{story[:description]}"
|
28
|
-
formatted << " - #{story_attributes.join(' ')}" unless story_attributes.empty?
|
29
|
-
formatted << "\n"
|
30
|
-
formatted << "#{prefix} #{story[:notes]}\n" if story[:notes]
|
31
|
-
story[:stories].each do |nested|
|
32
|
-
formatted << format_story(nested, :nested)
|
33
|
-
end if story[:stories]
|
34
|
-
formatted
|
35
|
-
end
|
36
|
-
|
37
|
-
def format_definition(definition)
|
38
|
-
[definition[:title], definition[:definition]].join(': ')
|
39
|
-
end
|
40
|
-
end
|