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 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