pdf-storycards 0.0.1 → 0.1.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.
- data/History.txt +7 -0
- data/Manifest.txt +9 -0
- data/README.txt +16 -11
- data/Rakefile +3 -2
- data/bin/stories2cards +31 -20
- data/examples/mingle_cards.rb +29 -0
- data/lib/pdf/storycards.rb +3 -1
- data/lib/pdf/storycards/story.rb +5 -1
- data/lib/pdf/storycards/story_capturing_mediator.rb +0 -3
- data/lib/pdf/storycards/story_parser.rb +41 -0
- data/lib/pdf/storycards/text_file_filter.rb +31 -0
- data/lib/pdf/storycards/writer.rb +42 -32
- data/spec/calculator_stories_with_metadata +56 -0
- data/spec/not_a_text_file.gif +0 -0
- data/spec/not_a_text_file.jpg +0 -0
- data/spec/pdf/storycards/story_parser_spec.rb +43 -0
- data/spec/pdf/storycards/text_file_filter_spec.rb +30 -0
- data/spec/pdf/storycards/writer_spec.rb +5 -11
- data/spec/spec_helper.rb +1 -0
- data/spec/subtraction_with_metadata +40 -0
- metadata +69 -67
data/History.txt
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
== 0.1 / 2008-02-09
|
2
|
+
|
3
|
+
* Reads stories from STDIN, writes PDF to STDOUT, makes for more Unix-friendly command line tool and more usable library class
|
4
|
+
* Turned off munging of story narrative into a sentence by default
|
5
|
+
* Introduced concept of metadata (colon-separated name value pairs) that are parsed out of tops of stories (before Story: ...) and the ability to specify metadata keys for the lower left and/or lower right of the cards -- the corresponding values will be printed there.
|
6
|
+
* Added a real-world example script: printing cards from Thoughtworks Studio's Mingle. In examples/mingle_cards.rb.
|
7
|
+
|
1
8
|
== 0.0.1 / 2007-12-27
|
2
9
|
|
3
10
|
* Initial release
|
data/Manifest.txt
CHANGED
@@ -3,15 +3,24 @@ Manifest.txt
|
|
3
3
|
README.txt
|
4
4
|
Rakefile
|
5
5
|
bin/stories2cards
|
6
|
+
examples/mingle_cards.rb
|
6
7
|
lib/pdf/storycards.rb
|
7
8
|
lib/pdf/storycards/scenario.rb
|
8
9
|
lib/pdf/storycards/story.rb
|
9
10
|
lib/pdf/storycards/story_capturing_mediator.rb
|
11
|
+
lib/pdf/storycards/story_parser.rb
|
12
|
+
lib/pdf/storycards/text_file_filter.rb
|
10
13
|
lib/pdf/storycards/writer.rb
|
11
14
|
spec/addition
|
12
15
|
spec/calculator_stories
|
16
|
+
spec/calculator_stories_with_metadata
|
17
|
+
spec/not_a_text_file.gif
|
18
|
+
spec/not_a_text_file.jpg
|
13
19
|
spec/pdf/storycards/story_capturing_mediator_spec.rb
|
20
|
+
spec/pdf/storycards/story_parser_spec.rb
|
14
21
|
spec/pdf/storycards/story_spec.rb
|
22
|
+
spec/pdf/storycards/text_file_filter_spec.rb
|
15
23
|
spec/pdf/storycards/writer_spec.rb
|
16
24
|
spec/rspec_suite.rb
|
17
25
|
spec/spec_helper.rb
|
26
|
+
spec/subtraction_with_metadata
|
data/README.txt
CHANGED
@@ -1,24 +1,29 @@
|
|
1
1
|
PDF StoryCards
|
2
|
-
|
3
|
-
http://
|
2
|
+
RDoc <http://pdf-storycards.rubyforge.org/>
|
3
|
+
Tracker <http://rubyforge.org/projects/pdf-storycards/>
|
4
|
+
by Luke Melia <http://www.lukemelia.com/>
|
4
5
|
|
5
6
|
== DESCRIPTION:
|
6
7
|
|
7
|
-
Provides a script and library to
|
8
|
-
plain text story format and
|
9
|
-
index cards suitable for using in
|
8
|
+
Provides a script and library to parse stories saved in the RSpec
|
9
|
+
plain text story format and creates a PDF file with printable 3"x5"
|
10
|
+
index cards suitable for using in Agile planning and prioritization.
|
10
11
|
|
11
12
|
== FEATURES/PROBLEMS:
|
12
13
|
|
13
14
|
* Create a PDF with each page as a 3x5 sheet, or as 4 cards per 8.5 x 11 sheet
|
14
|
-
*
|
15
|
-
* TODO: Take a directory and find all stories in it
|
16
|
-
* TODO: Take stories via STDIN
|
15
|
+
* Included script reads stories from STDIN and writes PDF to STDOUT
|
17
16
|
* TODO: Improve test coverage
|
17
|
+
* TODO: Improve documentation
|
18
18
|
|
19
19
|
== SYNOPSIS:
|
20
20
|
|
21
|
-
|
21
|
+
From the command line with
|
22
|
+
stories2cards < /path/to/stories.txt
|
23
|
+
|
24
|
+
Or via Ruby
|
25
|
+
story_text = File.read('my_story')
|
26
|
+
pdf_content = PDF::Storycards::Writer.make_pdf(story_text, :style => :card_1up)
|
22
27
|
|
23
28
|
== REQUIREMENTS:
|
24
29
|
|
@@ -27,13 +32,13 @@ index cards suitable for using in agile planning and prioritization.
|
|
27
32
|
|
28
33
|
== INSTALL:
|
29
34
|
|
30
|
-
sudo gem install storycards
|
35
|
+
sudo gem install pdf-storycards
|
31
36
|
|
32
37
|
== LICENSE:
|
33
38
|
|
34
39
|
(The MIT License)
|
35
40
|
|
36
|
-
Copyright (c) 2007 Luke Melia
|
41
|
+
Copyright (c) 2007-2008 Luke Melia
|
37
42
|
|
38
43
|
Permission is hereby granted, free of charge, to any person obtaining
|
39
44
|
a copy of this software and associated documentation files (the
|
data/Rakefile
CHANGED
@@ -13,11 +13,12 @@ Hoe.new('pdf-storycards', PDF::Storycards::VERSION) do |p|
|
|
13
13
|
p.author = 'Luke Melia'
|
14
14
|
p.email = 'luke@lukemelia.com'
|
15
15
|
p.summary = 'Utilities for generating printable story cards for agile planning and measurement'
|
16
|
-
p.description = p.paragraphs_of('README.txt',
|
17
|
-
|
16
|
+
p.description = p.paragraphs_of('README.txt', 1..5).join("\n\n")
|
17
|
+
p.url = 'http://rubyforge.org/projects/pdf-storycards/'
|
18
18
|
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
19
19
|
p.extra_deps << ['pdf-writer', '>= 1.1.7']
|
20
20
|
p.extra_deps << ['rspec', '>= 1.1.1']
|
21
|
+
p.remote_rdoc_dir = '' # Release to root
|
21
22
|
end
|
22
23
|
|
23
24
|
# vim: syntax=Ruby
|
data/bin/stories2cards
CHANGED
@@ -9,31 +9,34 @@
|
|
9
9
|
# This command outputs a PDF consisting of one card for each story and
|
10
10
|
# narrative defined in stories.txt and saves it as stories.pdf
|
11
11
|
#
|
12
|
-
# stories2cards stories.txt
|
12
|
+
# stories2cards < stories.txt
|
13
13
|
#
|
14
14
|
# Other examples
|
15
15
|
#
|
16
|
-
# stories2cards
|
17
|
-
# stories2cards stories.txt
|
16
|
+
# stories2cards -o /tmp/cards.pdf < stories.txt
|
17
|
+
# stories2cards --style=4up < stories.txt
|
18
18
|
#
|
19
19
|
# == Usage
|
20
|
-
# stories2cards [options] text_file_with_stories
|
20
|
+
# stories2cards [options] < text_file_with_stories
|
21
21
|
#
|
22
22
|
# For help use stories2cards -h
|
23
23
|
#
|
24
24
|
# == Options
|
25
25
|
# -h, --help Displays help message
|
26
26
|
# -v, --version Display the version, then exit
|
27
|
-
# -o, --output=PATH Specify the path to the PDF file
|
28
|
-
# to be created,
|
27
|
+
# -o, --output=PATH (optional) Specify the path to the PDF file
|
28
|
+
# to be created, writes to stdout by default
|
29
29
|
# -s, --style=1up|4up Defaults to 1up
|
30
|
-
# -
|
30
|
+
# -l, --lowerleft=key (optional) MetaData key for value to be printed in lower left
|
31
|
+
# -r, --lowerright=key (optional) MetaData key for value to be printed in lower right
|
32
|
+
# -b, --[no-]borders Print borders (only has effect with 4up style)
|
33
|
+
# -m, --makesentences, Make english narratives into sentences
|
31
34
|
#
|
32
35
|
# == Author
|
33
36
|
# Luke Melia
|
34
37
|
#
|
35
38
|
# == Copyright
|
36
|
-
# Copyright (c) 2007 Luke Melia. Licensed under the MIT License
|
39
|
+
# Copyright (c) 2007-2008 Luke Melia. Licensed under the MIT License
|
37
40
|
# http://www.opensource.org/licenses/mit-license.php
|
38
41
|
|
39
42
|
require 'optparse'
|
@@ -56,20 +59,21 @@ class App
|
|
56
59
|
|
57
60
|
attr_reader :options
|
58
61
|
|
59
|
-
def initialize(arguments, stdin)
|
62
|
+
def initialize(arguments, stdin, stdout)
|
60
63
|
@arguments = arguments
|
61
64
|
@stdin = stdin
|
65
|
+
@stdout = stdout
|
62
66
|
|
63
67
|
# Set defaults
|
64
68
|
@options = OpenStruct.new
|
65
|
-
@options.output = "storycards.pdf"
|
66
69
|
@options.style = :"1up"
|
67
70
|
end
|
68
71
|
|
69
72
|
# Parse options, check arguments, then process the command
|
70
73
|
def run
|
71
74
|
if parsed_options? && arguments_valid?
|
72
|
-
process_arguments
|
75
|
+
process_arguments
|
76
|
+
@story_text = @stdin.read
|
73
77
|
process_command
|
74
78
|
else
|
75
79
|
output_usage
|
@@ -86,7 +90,10 @@ class App
|
|
86
90
|
opts.on('-h', '--help') { output_help }
|
87
91
|
opts.on('-o', '--output [PATH]') { |path| @options.output = path }
|
88
92
|
opts.on('-s', '--style [STYLE]', [:"1up", :"4up"]) { |style| @options.style = style }
|
89
|
-
opts.on(
|
93
|
+
opts.on('-l', '--lowerleft [METADATA_KEY]') { |metadata_key| @options.lower_left = metadata_key }
|
94
|
+
opts.on('-r', '--lowerright [METADATA_KEY]') { |metadata_key| @options.lower_right = metadata_key }
|
95
|
+
opts.on("-b", "--[no-]borders", "Print borders") { |b| @options.borders = b }
|
96
|
+
opts.on('-m', '--makesentences', "Make english narratives into sentences") { |m| @options.make_sentences = m }
|
90
97
|
opts.parse!(@arguments) rescue return false
|
91
98
|
|
92
99
|
true
|
@@ -94,14 +101,12 @@ class App
|
|
94
101
|
|
95
102
|
# True if required arguments were provided
|
96
103
|
def arguments_valid?
|
97
|
-
return false
|
98
|
-
return false unless File.exists?(@arguments[0])
|
104
|
+
return false if @arguments.length > 0
|
99
105
|
return true
|
100
106
|
end
|
101
107
|
|
102
108
|
# Setup the arguments
|
103
109
|
def process_arguments
|
104
|
-
@source_file = @arguments[0]
|
105
110
|
end
|
106
111
|
|
107
112
|
def output_help
|
@@ -118,12 +123,18 @@ class App
|
|
118
123
|
end
|
119
124
|
|
120
125
|
def process_command
|
121
|
-
style = :card_1up if @options.style == :"1up"
|
122
|
-
style = :letter_4up if @options.style == :"4up"
|
123
|
-
|
126
|
+
@options.style = :card_1up if @options.style == :"1up"
|
127
|
+
@options.style = :letter_4up if @options.style == :"4up"
|
128
|
+
options_as_hash = @options.instance_variable_get('@table')
|
129
|
+
pdf_text = PDF::Storycards::Writer.make_pdf(@story_text, options_as_hash)
|
130
|
+
if @options.output
|
131
|
+
File.open(@options.output, 'w') {|f| f.write(pdf_text) }
|
132
|
+
else
|
133
|
+
@stdout.print pdf_text
|
134
|
+
end
|
124
135
|
end
|
125
|
-
|
136
|
+
end
|
126
137
|
|
127
138
|
# Create and run the application
|
128
|
-
app = App.new(ARGV,
|
139
|
+
app = App.new(ARGV, $stdin, $stdout)
|
129
140
|
app.run
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_record'
|
3
|
+
require 'pdf/storycards'
|
4
|
+
|
5
|
+
ActiveRecord::Base.establish_connection(
|
6
|
+
:adapter => "mysql",
|
7
|
+
:host => "localhost",
|
8
|
+
:username => "root",
|
9
|
+
:password => "",
|
10
|
+
:database => "mingle"
|
11
|
+
)
|
12
|
+
|
13
|
+
target_sprint_number = 4
|
14
|
+
|
15
|
+
class WeplayLaunchCard < ActiveRecord::Base
|
16
|
+
end
|
17
|
+
|
18
|
+
cards = WeplayLaunchCard.find(:all, :conditions => ["card_type_name = 'story' AND cp_target_sprint_number = ?", target_sprint_number], :order => 'cp_storyrank')
|
19
|
+
stories = cards.collect do |card|
|
20
|
+
story = "CardNumber: #{card.number}\n"
|
21
|
+
story << "Estimate: #{card.cp_estimated_effort}\n\n" unless card.cp_estimated_effort.nil?
|
22
|
+
story << "Story: #{card.name}\n\n"
|
23
|
+
story << " #{card.description.gsub("\n","\n ")}\n\n" unless card.description.nil?
|
24
|
+
story
|
25
|
+
end
|
26
|
+
story_text = stories.join(".\n")
|
27
|
+
|
28
|
+
pdf_text = PDF::Storycards::Writer.make_pdf(story_text, :style => :letter_4up, :borders => true, :lower_left => 'Estimate', :lower_right => 'CardNumber')
|
29
|
+
$stdout.print pdf_text
|
data/lib/pdf/storycards.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
module PDF
|
2
2
|
module Storycards
|
3
|
-
VERSION = '0.0
|
3
|
+
VERSION = '0.1.0'
|
4
4
|
end
|
5
5
|
end
|
6
6
|
|
7
|
+
require 'pdf/storycards/text_file_filter'
|
7
8
|
require 'pdf/storycards/scenario'
|
8
9
|
require 'pdf/storycards/story'
|
9
10
|
require 'pdf/storycards/story_capturing_mediator'
|
11
|
+
require 'pdf/storycards/story_parser'
|
10
12
|
require 'pdf/storycards/writer'
|
data/lib/pdf/storycards/story.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
module PDF
|
2
2
|
module Storycards
|
3
|
+
# The string Story#narrative is imbued with a method called to_sentence, which formats the
|
4
|
+
# story narrative as a single sentence without newlines.
|
3
5
|
class Story
|
4
6
|
def initialize(title, narrative)
|
7
|
+
@meta_data = {}
|
5
8
|
@title = title
|
6
9
|
@narrative = narrative
|
7
10
|
def @narrative.to_sentence
|
@@ -10,7 +13,8 @@ module PDF
|
|
10
13
|
@scenarios = []
|
11
14
|
end
|
12
15
|
|
13
|
-
|
16
|
+
attr_reader :title, :narrative
|
17
|
+
attr_accessor :meta_data
|
14
18
|
|
15
19
|
def add_scenario(scenario)
|
16
20
|
@scenarios << scenario
|
@@ -1,8 +1,5 @@
|
|
1
1
|
# RSpec's story parser needs to be constructed with a Mediator. The mediator defined in the class
|
2
2
|
# below simply collects stories and exposes the collection as StoryCapturingMediator#stories.
|
3
|
-
#
|
4
|
-
# It also sprinkles in a magic method on Story#narrative called to_sentence, which formats the
|
5
|
-
# story narrative as a single sentence without newlines.
|
6
3
|
module PDF
|
7
4
|
module Storycards
|
8
5
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module PDF
|
2
|
+
module Storycards
|
3
|
+
class StoryParser
|
4
|
+
|
5
|
+
def parse_stories(story_text)
|
6
|
+
all_stories = []
|
7
|
+
story_groups = story_text.split(/^\.$/)
|
8
|
+
story_groups.each do |story_group|
|
9
|
+
all_stories << parse_story_group(story_group)
|
10
|
+
end
|
11
|
+
return all_stories.flatten
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse_story_group(story_text)
|
15
|
+
meta_data = extract_meta_data(story_text)
|
16
|
+
story_mediator = StoryCapturingMediator.new
|
17
|
+
rspec_story_parser = Spec::Story::Runner::StoryParser.new(story_mediator)
|
18
|
+
rspec_story_parser.parse(story_text.split("\n"))
|
19
|
+
stories = story_mediator.stories
|
20
|
+
stories.each do |story|
|
21
|
+
story.meta_data = meta_data
|
22
|
+
end
|
23
|
+
stories
|
24
|
+
end
|
25
|
+
|
26
|
+
def extract_meta_data(story_text)
|
27
|
+
meta_data = {}
|
28
|
+
story_text.split("\n").each do |line|
|
29
|
+
match_data = /^([^ :]+):(.*)$/.match(line)
|
30
|
+
unless match_data.nil?
|
31
|
+
name = match_data[1].strip
|
32
|
+
value = match_data[2].strip
|
33
|
+
break if name == 'Story'
|
34
|
+
meta_data[name] = value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
meta_data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module PDF
|
2
|
+
module Storycards
|
3
|
+
class TextFileFilter
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(*paths)
|
7
|
+
@paths = paths
|
8
|
+
@text_files = paths.select{|path| nonbinary?(path)}
|
9
|
+
@text_files.map!{ |path| Pathname.new(path).cleanpath.to_s }
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
@text_files.each(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
NON_ASCII_PRINTABLE = /[^\x20-\x7e\s]/
|
19
|
+
|
20
|
+
def nonbinary?(path, size = 1024)
|
21
|
+
open(path) do |io|
|
22
|
+
while buf = io.read(size)
|
23
|
+
return false if NON_ASCII_PRINTABLE =~ buf
|
24
|
+
end
|
25
|
+
return true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -5,12 +5,14 @@ module PDF
|
|
5
5
|
module Storycards
|
6
6
|
class Writer
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
CARD_WIDTH = PDF::Writer.in2pts(5)
|
9
|
+
CARD_HEIGHT = PDF::Writer.in2pts(3)
|
10
|
+
|
11
|
+
def self.make_pdf( story_text, opts )
|
10
12
|
opts[:style] = :card_1up unless opts[:style]
|
11
|
-
opts[:
|
13
|
+
opts[:borders] = false unless opts[:borders]
|
12
14
|
|
13
|
-
stories =
|
15
|
+
stories = StoryParser.new.parse_stories(story_text)
|
14
16
|
|
15
17
|
if opts[:style] == :card_1up
|
16
18
|
pdf = PDF::Writer.new(:paper => [7.62, 12.7], :orientation => :landscape) #3x5 card
|
@@ -18,18 +20,16 @@ module PDF
|
|
18
20
|
pdf.move_pointer(-60)
|
19
21
|
|
20
22
|
stories.each_with_index do |story, index|
|
21
|
-
pdf.
|
22
|
-
|
23
|
+
pdf.start_new_page if index > 0
|
24
|
+
print_card_corners(pdf, story, opts[:lower_left], opts[:lower_right], 0, 0)
|
25
|
+
print_card_text(pdf, story, 0, opts[:make_sentences])
|
23
26
|
end
|
24
27
|
|
25
|
-
pdf.
|
26
|
-
return output_path
|
28
|
+
return pdf.render
|
27
29
|
|
28
30
|
elsif opts[:style] == :letter_4up
|
29
31
|
|
30
32
|
pdf = PDF::Writer.new(:paper => "LETTER", :orientation => :landscape)
|
31
|
-
card_width = PDF::Writer.in2pts(5)
|
32
|
-
card_height = PDF::Writer.in2pts(3)
|
33
33
|
margin = PDF::Writer.in2pts(0.33)
|
34
34
|
gutter = margin
|
35
35
|
padding = 10
|
@@ -37,54 +37,64 @@ module PDF
|
|
37
37
|
pdf.start_columns(2, gutter)
|
38
38
|
stories.each_with_index do |story, index|
|
39
39
|
|
40
|
-
is_laying_out_left_hand_column = (index < 2)
|
40
|
+
is_laying_out_left_hand_column = (index % 4 < 2)
|
41
41
|
if is_laying_out_left_hand_column
|
42
42
|
rect_x = margin
|
43
43
|
else
|
44
|
-
rect_x = pdf.page_width - margin -
|
44
|
+
rect_x = pdf.page_width - margin - CARD_WIDTH
|
45
45
|
end
|
46
46
|
|
47
47
|
is_laying_out_top_row = (index % 2 == 0)
|
48
48
|
if is_laying_out_top_row
|
49
49
|
pdf.start_new_page unless index == 0
|
50
50
|
pdf.y = pdf.page_height - margin - padding
|
51
|
-
rect_y = pdf.page_height - margin -
|
51
|
+
rect_y = pdf.page_height - margin - CARD_HEIGHT
|
52
52
|
else
|
53
53
|
rect_y = margin
|
54
|
-
pdf.y = margin +
|
54
|
+
pdf.y = margin + CARD_HEIGHT - padding
|
55
55
|
end
|
56
56
|
|
57
|
-
print_border(pdf, rect_x, rect_y
|
58
|
-
|
57
|
+
print_border(pdf, rect_x, rect_y) if opts[:borders]
|
58
|
+
print_card_corners(pdf, story, opts[:lower_left], opts[:lower_right], rect_x, rect_y)
|
59
|
+
print_card_text(pdf, story, padding, opts[:make_sentences])
|
59
60
|
end
|
60
61
|
|
61
|
-
pdf.
|
62
|
-
return output_path
|
62
|
+
return pdf.render
|
63
63
|
|
64
|
+
else
|
65
|
+
raise "Unknown style: #{opts[:style]}"
|
64
66
|
end
|
65
67
|
end
|
66
68
|
private
|
67
69
|
|
68
|
-
def self.print_border(pdf, x, y
|
70
|
+
def self.print_border(pdf, x, y)
|
69
71
|
pdf.stroke_color(Color::RGB::Black)
|
70
72
|
pdf.stroke_style(PDF::Writer::StrokeStyle.new(1))
|
71
|
-
pdf.rectangle(
|
72
|
-
end
|
73
|
-
|
74
|
-
def self.parse_stories(path)
|
75
|
-
story_mediator = StoryCapturingMediator.new
|
76
|
-
story_parser = Spec::Story::Runner::StoryParser.new(story_mediator)
|
77
|
-
story_text = File.read(path)
|
78
|
-
story_parser.parse(story_text.split("\n"))
|
79
|
-
return story_mediator.stories
|
73
|
+
pdf.rectangle(x, y, CARD_WIDTH, CARD_HEIGHT).close_stroke
|
80
74
|
end
|
81
|
-
|
82
|
-
def self.print_card_text(pdf, story, padding = 0)
|
75
|
+
|
76
|
+
def self.print_card_text(pdf, story, padding = 0, make_sentences = false)
|
77
|
+
top = pdf.y
|
83
78
|
pdf.text "<b>#{story.title}</b>", :font_size => 18, :justification => :center,
|
84
79
|
:left => padding, :right => padding
|
85
80
|
pdf.move_pointer(12)
|
86
|
-
|
87
|
-
|
81
|
+
narrative = story.narrative
|
82
|
+
narrative = narrative.to_sentence if make_sentences
|
83
|
+
pdf.text narrative, :justification => :left, :font_size => 14,
|
84
|
+
:left => padding, :right => padding
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.print_card_corners(pdf, story, lower_left_meta_data_key, lower_right_meta_data_key, card_x, card_y)
|
88
|
+
text_y = card_y + 16 + 8
|
89
|
+
text_width = CARD_WIDTH/2 - PDF::Writer.in2pts(0.4)
|
90
|
+
if story.meta_data[lower_left_meta_data_key]
|
91
|
+
text_x = card_x + PDF::Writer.in2pts(0.4)
|
92
|
+
pdf.add_text_wrap text_x, text_y, text_width, story.meta_data[lower_left_meta_data_key], 16, :left
|
93
|
+
end
|
94
|
+
if story.meta_data[lower_right_meta_data_key]
|
95
|
+
text_x = card_x + CARD_WIDTH/2
|
96
|
+
pdf.add_text_wrap text_x, text_y, text_width, story.meta_data[lower_right_meta_data_key], 16, :right
|
97
|
+
end
|
88
98
|
end
|
89
99
|
end
|
90
100
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
This is a story about a calculator. The text up here above the Story: declaration
|
2
|
+
won't be processed, so you can write whatever you wish!
|
3
|
+
|
4
|
+
CardNumber: 12
|
5
|
+
Estimate: Very Easy
|
6
|
+
|
7
|
+
Story: simple multiplication
|
8
|
+
|
9
|
+
As an accountant
|
10
|
+
I want to multiply numbers
|
11
|
+
So that I can count beans
|
12
|
+
|
13
|
+
Scenario: multiply one and one
|
14
|
+
Given a value of 1
|
15
|
+
And a multiplier of 1
|
16
|
+
|
17
|
+
When the multiplier is applied to the value
|
18
|
+
|
19
|
+
Then the product should be 1
|
20
|
+
And the corks should be popped
|
21
|
+
|
22
|
+
.
|
23
|
+
CardNumber: 15
|
24
|
+
Estimate: Very Hard
|
25
|
+
|
26
|
+
Story: simple subtraction
|
27
|
+
|
28
|
+
As an accountant
|
29
|
+
I want to subtract numbers
|
30
|
+
So that I can count beans
|
31
|
+
|
32
|
+
Scenario: subtract one minus one
|
33
|
+
Given an argument of 1
|
34
|
+
And an argument of 1
|
35
|
+
|
36
|
+
When the second argument is subtracted from the first argument
|
37
|
+
|
38
|
+
Then the different should be 0
|
39
|
+
And the corks should be popped
|
40
|
+
|
41
|
+
Scenario: subtract two from five
|
42
|
+
Given an argument of 5
|
43
|
+
And an argument of 2
|
44
|
+
|
45
|
+
When the second argument is subtracted from the first argument
|
46
|
+
|
47
|
+
Then the difference should be 3
|
48
|
+
Then it should snow
|
49
|
+
|
50
|
+
Scenario: subtract two more
|
51
|
+
GivenScenario subtract two from five
|
52
|
+
And an argument of 2
|
53
|
+
|
54
|
+
When the result is reduced by the argument
|
55
|
+
|
56
|
+
Then the result should be 1
|
Binary file
|
Binary file
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../spec_helper.rb'
|
2
|
+
|
3
|
+
module PDF
|
4
|
+
class Writer
|
5
|
+
def save_as(path)
|
6
|
+
#no op
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe PDF::Storycards::Writer do
|
12
|
+
|
13
|
+
before(:all) do
|
14
|
+
@single_story_path = File.dirname(__FILE__) + '/../../addition'
|
15
|
+
@single_story_with_meta_data_path = File.dirname(__FILE__) + '/../../subtraction_with_metadata'
|
16
|
+
@multiple_stories_with_meta_data_path = File.dirname(__FILE__) + '/../../calculator_stories_with_metadata'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should parse an example plain text story file and create a new story" do
|
20
|
+
story_text = File.read(@single_story_path)
|
21
|
+
stories = PDF::Storycards::StoryParser.new.parse_stories(story_text)
|
22
|
+
stories.first.title.should == 'simple addition'
|
23
|
+
stories.first.narrative.should == "As an accountant\nI want to add numbers\nSo that I can count beans"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should parse extract metadata from plain text story file and set it on the story" do
|
27
|
+
story_text = File.read(@single_story_with_meta_data_path)
|
28
|
+
stories = PDF::Storycards::StoryParser.new.parse_stories(story_text)
|
29
|
+
stories.first.meta_data['CardNumber'].should == '152'
|
30
|
+
stories.first.meta_data['Estimate'].should == 'Medium'
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should treat a single . on a line as a separator between story groups" do
|
34
|
+
story_text = File.read(@multiple_stories_with_meta_data_path)
|
35
|
+
stories = PDF::Storycards::StoryParser.new.parse_stories(story_text)
|
36
|
+
stories.first.title.should == 'simple multiplication'
|
37
|
+
stories.first.meta_data['CardNumber'].should == '12'
|
38
|
+
stories.first.meta_data['Estimate'].should == 'Very Easy'
|
39
|
+
stories.last.title.should == 'simple subtraction'
|
40
|
+
stories.last.meta_data['CardNumber'].should == '15'
|
41
|
+
stories.last.meta_data['Estimate'].should == 'Very Hard'
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../spec_helper.rb'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
describe PDF::Storycards::TextFileFilter do
|
5
|
+
before(:all) do
|
6
|
+
@text_file_path_1 = File.dirname(__FILE__) + '/../../addition'
|
7
|
+
@text_file_clean_path_1 = Pathname.new(@text_file_path_1).cleanpath.to_s
|
8
|
+
@text_file_path_2 = File.dirname(__FILE__) + '/../../calculator_stories'
|
9
|
+
@non_text_file_path_1 = File.dirname(__FILE__) + '/../../not_a_text_file.jpg'
|
10
|
+
@non_text_file_clean_path_1 = Pathname.new(@non_text_file_path_1).cleanpath.to_s
|
11
|
+
@non_text_file_path_2 = File.dirname(__FILE__) + '/../../not_a_text_file.gif'
|
12
|
+
@directory = File.dirname(__FILE__) + '/../../../'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should expose an empty collection when initialized with nothing" do
|
16
|
+
filter = PDF::Storycards::TextFileFilter.new()
|
17
|
+
filter.map.should == []
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should expose a collection of the clean path to one text file when initialized with only that text file" do
|
21
|
+
filter = PDF::Storycards::TextFileFilter.new(@text_file_path_1)
|
22
|
+
filter.map.should == [@text_file_clean_path_1]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should expose an empty collection when initialized with a single non-text file" do
|
26
|
+
filter = PDF::Storycards::TextFileFilter.new(@non_text_file_path_1)
|
27
|
+
filter.map.should == []
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -11,28 +11,22 @@ end
|
|
11
11
|
describe PDF::Storycards::Writer do
|
12
12
|
|
13
13
|
before(:all) do
|
14
|
-
|
15
|
-
@
|
16
|
-
end
|
17
|
-
|
18
|
-
it "should parse an example plain text story file and create a new story" do
|
19
|
-
stories = PDF::Storycards::Writer.send(:parse_stories, @single_story_path)
|
20
|
-
stories.first.title.should == 'simple addition'
|
21
|
-
stories.first.narrative.should == "As an accountant\nI want to add numbers\nSo that I can count beans"
|
14
|
+
multiple_stories_path = File.dirname(__FILE__) + '/../../calculator_stories'
|
15
|
+
@story_text = File.read(multiple_stories_path)
|
22
16
|
end
|
23
17
|
|
24
18
|
it "should not print border when printing 1-up" do
|
25
19
|
PDF::Storycards::Writer.should_not_receive(:print_border)
|
26
|
-
PDF::Storycards::Writer.make_pdf(@
|
20
|
+
PDF::Storycards::Writer.make_pdf(@story_text, :output => 'storycards.pdf', :style => :card_1up)
|
27
21
|
end
|
28
22
|
|
29
23
|
it "should not print border when printing 4-up with default options" do
|
30
24
|
PDF::Storycards::Writer.should_not_receive(:print_border)
|
31
|
-
PDF::Storycards::Writer.make_pdf(@
|
25
|
+
PDF::Storycards::Writer.make_pdf(@story_text, :output => 'storycards.pdf', :style => :letter_4up)
|
32
26
|
end
|
33
27
|
|
34
28
|
it "should print border when printing 4-up with border option set to true" do
|
35
29
|
PDF::Storycards::Writer.should_receive(:print_border).twice
|
36
|
-
PDF::Storycards::Writer.make_pdf(@
|
30
|
+
PDF::Storycards::Writer.make_pdf(@story_text, :output => 'storycards.pdf', :style => :letter_4up, :borders => true)
|
37
31
|
end
|
38
32
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
This is a story about a calculator. The text up here above the Story: declaration
|
2
|
+
won't be processed, so you can write whatever you wish!
|
3
|
+
|
4
|
+
There is some metadata below. It's also above the Story: declaration, so it won't be
|
5
|
+
processed by storyrunner, but pdf-storycards can work with it.
|
6
|
+
|
7
|
+
CardNumber: 152
|
8
|
+
Estimate: Medium
|
9
|
+
|
10
|
+
Story: simple subtraction
|
11
|
+
|
12
|
+
As an accountant
|
13
|
+
I want to subtract numbers
|
14
|
+
So that I can count beans
|
15
|
+
|
16
|
+
Scenario: subtract one minus one
|
17
|
+
Given an argument of 1
|
18
|
+
And an argument of 1
|
19
|
+
|
20
|
+
When the second argument is subtracted from the first argument
|
21
|
+
|
22
|
+
Then the different should be 0
|
23
|
+
And the corks should be popped
|
24
|
+
|
25
|
+
Scenario: subtract two from five
|
26
|
+
Given an argument of 5
|
27
|
+
And an argument of 2
|
28
|
+
|
29
|
+
When the second argument is subtracted from the first argument
|
30
|
+
|
31
|
+
Then the difference should be 3
|
32
|
+
Then it should snow
|
33
|
+
|
34
|
+
Scenario: subtract two more
|
35
|
+
GivenScenario subtract two from five
|
36
|
+
And an argument of 2
|
37
|
+
|
38
|
+
When the result is reduced by the argument
|
39
|
+
|
40
|
+
Then the result should be 1
|
metadata
CHANGED
@@ -1,21 +1,80 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
2
4
|
name: pdf-storycards
|
3
5
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2008-02-10 00:00:00 -05:00
|
8
|
+
summary: Utilities for generating printable story cards for agile planning and measurement
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: luke@lukemelia.com
|
12
|
+
homepage: http://rubyforge.org/projects/pdf-storycards/
|
13
|
+
rubyforge_project: pdf-storycards
|
14
|
+
description: "== DESCRIPTION: Provides a script and library to parse stories saved in the RSpec plain text story format and creates a PDF file with printable 3\"x5\" index cards suitable for using in Agile planning and prioritization. == FEATURES/PROBLEMS: * Create a PDF with each page as a 3x5 sheet, or as 4 cards per 8.5 x 11 sheet * Included script reads stories from STDIN and writes PDF to STDOUT * TODO: Improve test coverage * TODO: Improve documentation == SYNOPSIS: From the command line with stories2cards < /path/to/stories.txt Or via Ruby story_text = File.read('my_story') pdf_content = PDF::Storycards::Writer.make_pdf(story_text, :style => :card_1up) == REQUIREMENTS:"
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
5
25
|
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
6
29
|
authors:
|
7
30
|
- Luke Melia
|
8
|
-
|
9
|
-
|
10
|
-
|
31
|
+
files:
|
32
|
+
- History.txt
|
33
|
+
- Manifest.txt
|
34
|
+
- README.txt
|
35
|
+
- Rakefile
|
36
|
+
- bin/stories2cards
|
37
|
+
- examples/mingle_cards.rb
|
38
|
+
- lib/pdf/storycards.rb
|
39
|
+
- lib/pdf/storycards/scenario.rb
|
40
|
+
- lib/pdf/storycards/story.rb
|
41
|
+
- lib/pdf/storycards/story_capturing_mediator.rb
|
42
|
+
- lib/pdf/storycards/story_parser.rb
|
43
|
+
- lib/pdf/storycards/text_file_filter.rb
|
44
|
+
- lib/pdf/storycards/writer.rb
|
45
|
+
- spec/addition
|
46
|
+
- spec/calculator_stories
|
47
|
+
- spec/calculator_stories_with_metadata
|
48
|
+
- spec/not_a_text_file.gif
|
49
|
+
- spec/not_a_text_file.jpg
|
50
|
+
- spec/pdf/storycards/story_capturing_mediator_spec.rb
|
51
|
+
- spec/pdf/storycards/story_parser_spec.rb
|
52
|
+
- spec/pdf/storycards/story_spec.rb
|
53
|
+
- spec/pdf/storycards/text_file_filter_spec.rb
|
54
|
+
- spec/pdf/storycards/writer_spec.rb
|
55
|
+
- spec/rspec_suite.rb
|
56
|
+
- spec/spec_helper.rb
|
57
|
+
- spec/subtraction_with_metadata
|
58
|
+
test_files: []
|
59
|
+
|
60
|
+
rdoc_options:
|
61
|
+
- --main
|
62
|
+
- README.txt
|
63
|
+
extra_rdoc_files:
|
64
|
+
- History.txt
|
65
|
+
- Manifest.txt
|
66
|
+
- README.txt
|
67
|
+
executables:
|
68
|
+
- stories2cards
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
requirements: []
|
11
72
|
|
12
|
-
date: 2007-12-29 00:00:00 -05:00
|
13
|
-
default_executable:
|
14
73
|
dependencies:
|
15
74
|
- !ruby/object:Gem::Dependency
|
16
75
|
name: pdf-writer
|
17
76
|
version_requirement:
|
18
|
-
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
19
78
|
requirements:
|
20
79
|
- - ">="
|
21
80
|
- !ruby/object:Gem::Version
|
@@ -24,7 +83,7 @@ dependencies:
|
|
24
83
|
- !ruby/object:Gem::Dependency
|
25
84
|
name: rspec
|
26
85
|
version_requirement:
|
27
|
-
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
28
87
|
requirements:
|
29
88
|
- - ">="
|
30
89
|
- !ruby/object:Gem::Version
|
@@ -33,66 +92,9 @@ dependencies:
|
|
33
92
|
- !ruby/object:Gem::Dependency
|
34
93
|
name: hoe
|
35
94
|
version_requirement:
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
37
96
|
requirements:
|
38
97
|
- - ">="
|
39
98
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.
|
99
|
+
version: 1.5.0
|
41
100
|
version:
|
42
|
-
description: "== FEATURES/PROBLEMS: * Create a PDF with each page as a 3x5 sheet, or as 4 cards per 8.5 x 11 sheet * Currently reads stories from a single file. * TODO: Take a directory and find all stories in it * TODO: Take stories via STDIN * TODO: Improve test coverage == SYNOPSIS: StorycardPdfWriter.make_pdf(\"/tmp/stories.txt\", \"/tmp/storycards.pdf\", :style => :card_1up) == REQUIREMENTS:"
|
43
|
-
email: luke@lukemelia.com
|
44
|
-
executables:
|
45
|
-
- stories2cards
|
46
|
-
extensions: []
|
47
|
-
|
48
|
-
extra_rdoc_files:
|
49
|
-
- History.txt
|
50
|
-
- Manifest.txt
|
51
|
-
- README.txt
|
52
|
-
files:
|
53
|
-
- History.txt
|
54
|
-
- Manifest.txt
|
55
|
-
- README.txt
|
56
|
-
- Rakefile
|
57
|
-
- bin/stories2cards
|
58
|
-
- lib/pdf/storycards.rb
|
59
|
-
- lib/pdf/storycards/scenario.rb
|
60
|
-
- lib/pdf/storycards/story.rb
|
61
|
-
- lib/pdf/storycards/story_capturing_mediator.rb
|
62
|
-
- lib/pdf/storycards/writer.rb
|
63
|
-
- spec/addition
|
64
|
-
- spec/calculator_stories
|
65
|
-
- spec/pdf/storycards/story_capturing_mediator_spec.rb
|
66
|
-
- spec/pdf/storycards/story_spec.rb
|
67
|
-
- spec/pdf/storycards/writer_spec.rb
|
68
|
-
- spec/rspec_suite.rb
|
69
|
-
- spec/spec_helper.rb
|
70
|
-
has_rdoc: true
|
71
|
-
homepage: http://www.zenspider.com/ZSS/Products/pdf-storycards/
|
72
|
-
post_install_message:
|
73
|
-
rdoc_options:
|
74
|
-
- --main
|
75
|
-
- README.txt
|
76
|
-
require_paths:
|
77
|
-
- lib
|
78
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: "0"
|
83
|
-
version:
|
84
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
-
requirements:
|
86
|
-
- - ">="
|
87
|
-
- !ruby/object:Gem::Version
|
88
|
-
version: "0"
|
89
|
-
version:
|
90
|
-
requirements: []
|
91
|
-
|
92
|
-
rubyforge_project: pdf-storycards
|
93
|
-
rubygems_version: 1.0.1
|
94
|
-
signing_key:
|
95
|
-
specification_version: 2
|
96
|
-
summary: Utilities for generating printable story cards for agile planning and measurement
|
97
|
-
test_files: []
|
98
|
-
|