saga 0.11.1 → 0.12.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.
- 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
|