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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c513dd09debeec529175004b691ad44ca271fd57
4
- data.tar.gz: b779878e3b8e252fc9b36db4e42f07dfee2beaed
2
+ SHA256:
3
+ metadata.gz: 79f90ac99939d798864d05b8925eea44ca51ff11d38f33e74a07c7baa3a69c0c
4
+ data.tar.gz: 7e1f6ad4ac27d39b0f2b74a83c291ea8cb93f99099f95d1acf6f38d2d41dffb4
5
5
  SHA512:
6
- metadata.gz: 071e3b875cb5597313c93ea2c7b64b2311931bfbdac261c2997c36bedd1fb1381c3ad479bf58efa8d63265e8fb46882220f7b7c898db2692877dd2d7dd785a40
7
- data.tar.gz: 6e27fddc6dfb04c4089b153681b343e3cbf138d48b7d6e32a46a15d2d1b672546ade76b1b94f5ab0bc9d56f6a3d39670d952a01e787e8bd4770a05282f1cc09c
6
+ metadata.gz: bda299077b1d0117ff16e927ad167fd6dc104e4e74a65870a173de96fa42b4b284b49ac6496174f24aebc4b20ec3254f4f5a41ea372a56a0b6c92449bd4b1e63
7
+ data.tar.gz: fc8e568db9e467b5fe47f250f11c4d78cdcffbd444947f33245632ec1a2cad4d3825565e4faaf4c373caf693b9b2e9e2402d935704d273576fde52e9a1683f3c
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Fingertips, Manfred Stienstra <manfred@fngtps.com>
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 = ActiveSupport::OrderedHash.new
12
- @definitions = ActiveSupport::OrderedHash.new
9
+ @stories = {}
10
+ @definitions = {}
13
11
  end
14
-
12
+
15
13
  def copy_story(story)
16
14
  copied = {}
17
- [:id, :iteration, :status, :estimate, :description].each do |attribute|
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.inject([]) do |ids, stories|
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 if story[:stories]
49
- end; ids
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 |section, data|
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
@@ -1,42 +1,70 @@
1
- require 'erubis'
1
+ require 'erb'
2
2
 
3
3
  module Saga
4
4
  class Formatter
5
- TEMPLATE_PATH = File.expand_path('../../../templates', __FILE__)
6
-
7
- def initialize(document, options={})
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
- @options = options
10
- @options[:template] ||= File.join(self.class.template_path, 'default')
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
- helpers_file = File.join(@options[:template], 'helpers.rb')
20
+ @document.extend(ERB::Util) unless @document.is_a?(ERB::Util)
21
+
15
22
  if File.exist?(helpers_file)
16
- load helpers_file
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, options={})
30
- formatter = new(document, options)
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, :template => File.join(template_path, 'saga'))
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 :title == @current_section
61
+
62
+ if @current_section == :title
62
63
  @document.title = string.gsub(/^requirements/i, '').strip
63
64
  self.current_section = :introduction
64
- elsif :introduction == @current_section
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 = {:story_count => 0, :estimate_total_in_hours => 0}
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.inject({}) do |properties, story|
11
- if story[:estimate]
12
- iteration = story[:iteration] || -1
13
- properties[iteration] ||= BLANK_ITERATION.dup
14
- properties[iteration][:story_count] += 1
15
- properties[iteration][:estimate_total_in_hours] += self.class.estimate_to_hours(story[:estimate])
16
- end
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 |iteration, properties|
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] and story[:status]
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
- "There are no stories yet."
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 or !statusses.empty?
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
- label = (iteration == -1) ? "Unplanned" : "Iteration #{iteration}"
100
- else
101
- label = 'Total'
102
- end
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 |opts|
12
- opts.banner = "Usage: saga [command]"
13
- opts.separator ""
14
- opts.separator "Commands:"
15
- opts.separator " new - prints a blank stub"
16
- opts.separator " convert <filename> - convert the stories to HTML"
17
- opts.separator " inspect <filename> - print the internals of the document"
18
- opts.separator " autofill <filename> - adds an id to stories without one"
19
- opts.separator " planning <filename> - shows the planning of stories in iterations"
20
- opts.separator " template <dir> - creates a template directory"
21
- opts.separator ""
22
- opts.separator "Options:"
23
- opts.on("-t", "--template DIR", "Use an external template for conversion to HTML") do |template_path|
24
- @options[:template] = File.expand_path(template_path)
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
- opts.on("-h", "--help", "Show help") do
27
- puts opts
28
- exit
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 << self.class.author
44
+ document.authors << author
37
45
  document.stories[''] = [{
38
- :description => 'As a writer I would like to write stories so developers can implement them.',
39
- :id => 1,
40
- :status => 'todo'
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
- :title => 'Writer',
44
- :definition => 'Someone who is responsible for writing down requirements in the form of stories'
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 convert(filename, options)
51
- Saga::Formatter.format(Saga::Parser.parse(File.read(filename)), options)
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, options)
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]), options)
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), options)
114
+ puts convert(File.expand_path(command))
101
115
  end
102
116
  end
103
-
117
+
104
118
  def run
105
- parser.parse!(@argv)
119
+ return unless options.run
120
+
106
121
  if command = @argv.shift
107
- run_command(command, @options)
122
+ run_command(command)
108
123
  else
109
124
  puts parser.to_s
110
125
  end
111
126
  end
112
-
113
- def self.author
114
- name = `osascript -e "long user name of (system info)" &1> /dev/null`.strip
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
@@ -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
- @part = :current_section
10
+ @current_section = nil
11
11
  end
12
-
12
+
13
13
  def expect_stories?
14
- %w(story stories).include?(@current_section.to_s)
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 Exception => exception
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
- { :description => input.strip }
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
- {:title => match[1], :definition => match[2]}
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Saga
4
+ VERSION = '0.12.0'.freeze
5
+ end
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.11.1
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: 2015-07-03 00:00:00.000000000 Z
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: '2.3'
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: '0'
26
+ version: '5'
55
27
  - !ruby/object:Gem::Dependency
56
- name: mocha-on-bacon
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: Saga is a tool to convert stories syntax to a nicely formatted document.
70
- email: manfred@fngtps.com
71
- executables:
72
- - saga
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
- - templates/default/document.erb
91
- - templates/default/helpers.rb
92
- - templates/requirements.txt.erb
93
- - templates/saga/document.erb
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
- rubyforge_project:
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,5 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- $:.unshift(File.expand_path('../../lib', __FILE__))
4
- require 'saga'
5
- Saga.run(ARGV)
@@ -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 %> &middot; 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 &amp; 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
@@ -1,7 +0,0 @@
1
- Requirements <%= title %>
2
-
3
- - <%= author %>
4
-
5
- USER STORIES
6
-
7
- As a developer I would like to write stories so I know what to develop. - #1 todo
@@ -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 %>
@@ -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