pdf-storycards 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
|