contraption 0.2.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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +33 -0
  7. data/Rakefile +12 -0
  8. data/bin/contraption +7 -0
  9. data/contraption.gemspec +29 -0
  10. data/features/author_finalizes_draft.feature +21 -0
  11. data/features/author_generates_site.feature +35 -0
  12. data/features/step_definitions/author_steps.rb +52 -0
  13. data/features/support/env.rb +0 -0
  14. data/features/support/example_inputs.rb +188 -0
  15. data/features/support/hooks.rb +3 -0
  16. data/lib/contraption/catalog.rb +65 -0
  17. data/lib/contraption/formatter.rb +30 -0
  18. data/lib/contraption/header.rb +83 -0
  19. data/lib/contraption/http_handler.rb +11 -0
  20. data/lib/contraption/location.rb +81 -0
  21. data/lib/contraption/options.rb +56 -0
  22. data/lib/contraption/post.rb +69 -0
  23. data/lib/contraption/repository.rb +58 -0
  24. data/lib/contraption/rss_builder.rb +30 -0
  25. data/lib/contraption/runner.rb +60 -0
  26. data/lib/contraption/s3_uploader.rb +30 -0
  27. data/lib/contraption/site.rb +85 -0
  28. data/lib/contraption/tag.rb +20 -0
  29. data/lib/contraption/tag_cloud.rb +35 -0
  30. data/lib/contraption/version.rb +3 -0
  31. data/lib/contraption.rb +14 -0
  32. data/spec/contraption/lib/catalog_spec.rb +105 -0
  33. data/spec/contraption/lib/formatter_spec.rb +30 -0
  34. data/spec/contraption/lib/header_spec.rb +198 -0
  35. data/spec/contraption/lib/location_spec.rb +148 -0
  36. data/spec/contraption/lib/options_spec.rb +25 -0
  37. data/spec/contraption/lib/post_spec.rb +50 -0
  38. data/spec/contraption/lib/repository_spec.rb +38 -0
  39. data/spec/contraption/lib/tag_cloud_spec.rb +39 -0
  40. data/spec/contraption/lib/tag_spec.rb +38 -0
  41. data/spec/contraption/lib/version_spec.rb +9 -0
  42. data/spec/spec_helper.rb +6 -0
  43. metadata +201 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d1c34da43377fb2b0cec9e0c8cf821867a18a85b
4
+ data.tar.gz: 8623c83b4113c1878756257ea4f43f02d8fa8ab4
5
+ SHA512:
6
+ metadata.gz: 873f12c0aa72c64145879a0b09564419aef292d60fd3eee8d00553c9639279ec353b21a0c5bc77e3558ad6dc8ab7223f34b186b98263daff0c847aa1c20e4d4a
7
+ data.tar.gz: 78c1b4f321c8327e687f542dfc508a0f4bf8df61ed8e71a4f463fd1a5e2bdced4b9b5bf82987d5e642c5bd5f7b721e38612a2b6f2c248eceb18e7e001f195fb9
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ language: ruby
3
+ rvm:
4
+ - 2.0.0
5
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in contraption.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Casey Robinson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # Contraption
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'contraption'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install contraption
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+ [![Code Climate](https://codeclimate.com/github/rampantmonkey/contraption.png)](https://codeclimate.com/github/rampantmonkey/contraption)
25
+ [![Build Status](https://travis-ci.org/rampantmonkey/contraption.png?branch=master)](https://travis-ci.org/rampantmonkey/contraption)
26
+ [![Coverage Status](https://coveralls.io/repos/rampantmonkey/contraption/badge.png?branch=master)](https://coveralls.io/r/rampantmonkey/contraption?branch=master)
27
+ ## Contributing
28
+
29
+ 1. Fork it
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ require 'cucumber/rake/task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ Cucumber::Rake::Task.new(:features) do |t|
8
+ t.cucumber_opts = "features --format pretty"
9
+ end
10
+
11
+ task :default => [:spec, :features]
12
+
data/bin/contraption ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'contraption/options'
4
+ require 'contraption/runner'
5
+
6
+ options = Contraption::Options.new ARGV
7
+ Contraption::Runner.new(options).run!
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'contraption/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "contraption"
8
+ spec.version = Contraption::VERSION
9
+ spec.authors = ["Casey Robinson"]
10
+ spec.email = ["kc@rampantmonkey.com"]
11
+ spec.description = %q{Static site generator}
12
+ spec.summary = %q{Static site generator}
13
+ spec.homepage = "http://rampantmonkey.com"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec", "~> 2.13"
24
+ spec.add_development_dependency "cucumber", "~> 1.3"
25
+ spec.add_development_dependency "coveralls", "~> 0.6"
26
+
27
+ spec.add_dependency "redcarpet", "~> 2.3"
28
+ spec.add_dependency "aws-s3", "~> 0.6.3"
29
+ end
@@ -0,0 +1,21 @@
1
+ Feature: Author finalizes draft
2
+
3
+ As an author
4
+ I have a draft
5
+ That I want to finalize for publication
6
+
7
+ Scenario: finalize draft
8
+ Given I have a valid project
9
+ Given I have a completed draft
10
+ Given I have a raw format
11
+ Given I have a article format
12
+ Given I have a page format
13
+ Given I have a month format
14
+ Given I have a year format
15
+ Given I have a tag format
16
+ Given I have a tag_cloud format
17
+ Given I have a landing_page format
18
+ When I run contraption
19
+ Then I should have a new post
20
+ And The post should be the formatted version of the draft
21
+ And The draft should be removed
@@ -0,0 +1,35 @@
1
+ Feature: Author generates website
2
+
3
+ As an author
4
+ I have many posts
5
+ That I want to publish
6
+
7
+ Scenario: generate individual posts
8
+ Given I have a valid project
9
+ Given I have a raw format
10
+ Given I have a article format
11
+ Given I have a page format
12
+ Given I have a month format
13
+ Given I have a year format
14
+ Given I have a tag format
15
+ Given I have a tag_cloud format
16
+ Given I have a landing_page format
17
+ Given The project has a post titled "A sample post" from 3 Feb 2012 tagged funny
18
+ Given The project has a post titled "Purple People Eaters" from 1 Mar 2013 tagged funny
19
+ Given The project has a post titled "Flying Monkeys" from 8 Mar 2013 tagged strange
20
+ When I run contraption
21
+ Then The site should contain 2012/02/a-sample-post.html
22
+ Then The site should contain 2013/03/purple-people-eaters.html
23
+ Then The site should contain 2013/03/flying-monkeys.html
24
+ Then The site should contain 2012/02/index.html
25
+ Then The site should contain 2013/03/index.html
26
+ Then The site should contain 2012/index.html
27
+ Then The site should contain 2013/index.html
28
+ Then The site should contain tags/funny/index.html
29
+ Then The site should contain tags/strange/index.html
30
+ Then The site should contain tags/index.html
31
+ Then The site should contain recent.html
32
+ And 'recent.html' should have "Flying Monkeys" before "Purple People Eaters".
33
+ Then The site should contain index.html
34
+ Then The site should contain rss
35
+
@@ -0,0 +1,52 @@
1
+ require 'pathname'
2
+
3
+ Given(/^I have a valid project$/) do
4
+ @root = Pathname.new('tmp/data')
5
+ @root.mkpath
6
+ @required_paths = %w[drafts posts formats].map{|p| @root + p}
7
+ @required_paths.each{|p| p.mkpath}
8
+ end
9
+
10
+ Given(/^I have a (\w+?) format$/) do |format_name|
11
+ create_format "#{format_name}", @root + 'formats'
12
+ end
13
+
14
+ Given(/^I have a completed draft$/) do
15
+ @post_name = 'testing-testing.md'
16
+ create_test_draft @post_name, (@root + 'drafts')
17
+ end
18
+
19
+ When(/^I run contraption$/) do
20
+ @output = @root + '..' + 'output'
21
+ `ruby -Ilib bin/contraption -s #{@root} -d #{@output}`
22
+ end
23
+
24
+ Then(/^I should have a new post$/) do
25
+ @post_path = @root + 'posts' + "#{Date.today.strftime('%Y%m%d-')+@post_name}"
26
+ File.exist?(@post_path).should be_true
27
+ end
28
+
29
+ Then(/^The draft should be removed$/) do
30
+ File.exist?(@root + 'drafts' + @post_name).should be_false
31
+ end
32
+
33
+ Then(/^The post should be the formatted version of the draft$/) do
34
+ post_content = File.read(@post_path)
35
+ post_content.should_not include('Contraption::Post')
36
+ post_content.should_not include('Published: now')
37
+ end
38
+
39
+ Given(/^The project has a post titled "(.*)" from (\d+) (\S+) (\d+) tagged (\S+)$/) do |title, day, month, year, tags|
40
+ publication = "#{day} #{month} #{year}"
41
+ p = create_post_with published: publication, title: title, tags: tags
42
+ save_post p, @root + 'posts'
43
+ end
44
+
45
+ Then(/^The site should contain (.*)$/) do |path|
46
+ File.exist?(@output + path).should be_true
47
+ end
48
+
49
+ Then(/^'(.*?)' should have "(.*?)" before "(.*?)"\.$/) do |path, first_string, second_string|
50
+ content = File.read(@output+path)
51
+ content.index(first_string).should < content.index(second_string)
52
+ end
File without changes
@@ -0,0 +1,188 @@
1
+ require_relative '../../lib/contraption/header'
2
+ require_relative '../../lib/contraption/post'
3
+
4
+ def create_test_draft name, path
5
+ content =<<-POST
6
+ Testing... Testing...
7
+ Type: Article
8
+ Tags: Test
9
+ Summary: Testing.
10
+ Published: now
11
+
12
+ Testing whether the markdown support works.
13
+
14
+ Code sample
15
+ #include<iostream>
16
+
17
+ int main(){
18
+ std::cout << "Hello World.\\n";
19
+ return 0;
20
+ }
21
+
22
+ Some __bold text__.
23
+
24
+ And an image for good luck ![test](test2.jpg)
25
+ POST
26
+ write_file content, path+name
27
+ end
28
+
29
+ def create_post_with opts={}
30
+
31
+ end
32
+
33
+ def create_format name="raw", path
34
+ format = case name
35
+ when /article/
36
+ article_format
37
+ when /landing_page/
38
+ landing_page_format
39
+ when /page/
40
+ page_format
41
+ when /month/
42
+ month_format
43
+ when /year/
44
+ year_format
45
+ when /tag_cloud/
46
+ tag_cloud_format
47
+ when /tag/
48
+ tag_format
49
+ else
50
+ raw_format
51
+ end
52
+ write_file format, path+"#{name}.erb"
53
+ end
54
+
55
+ def raw_format
56
+ <<-'FORMAT'
57
+ <%=context.title%>
58
+ Type: <%=context.type%>
59
+ Tags: <%=context.tags%>
60
+ Published: <%=context.publication_date%>
61
+ Summary: <%=context.summary%>
62
+
63
+ <%=context.body%>
64
+ FORMAT
65
+ end
66
+
67
+ def month_format
68
+ <<-'FORMAT'
69
+ <h1 class=\"archive\">ARCHIVE FOR '#{context[:month].month} #{context[:month].year}'</h1>\n<hr class=\"subtleSep\" />
70
+ <%= context[:content] %>
71
+
72
+ FORMAT
73
+ end
74
+
75
+ def year_format
76
+ <<-'FORMAT'
77
+ <h1 class=\"archive\">ARCHIVE FOR '#{context[:year].year}'</h1>\n<hr class=\"subtleSep\" />
78
+ <%= context[:content] %>
79
+
80
+ FORMAT
81
+ end
82
+
83
+ def tag_format
84
+ <<-'FORMAT'
85
+ <h1 class-\"archive\"ARCHIVE FOR '#{context[:tag]}'</h1><\n<hr class=\"subtleSep\" />
86
+ <%= context[:content] %>
87
+ FORMAT
88
+ end
89
+
90
+ def tag_cloud_format
91
+ <<-'FORMAT'
92
+ <%= context[:content] %>
93
+ FORMAT
94
+ end
95
+
96
+ def article_format
97
+ <<-'FORMAT'
98
+ <article class="group">
99
+ <div class="grid14 thetitle">
100
+ <h1><%= context.title %></h1>
101
+ </div>
102
+ <div class="metadata grid2">
103
+ <span class="date">
104
+ <%= context.publication_date.strftime("%-d %b %Y")%>
105
+ </span>
106
+ <% context.tags.each do |t| %>
107
+ <span class="tag">
108
+ <a href="/tags/<%= t.to_s.gsub ' ', '_' %>">
109
+ <%= t.to_s %>
110
+ </a>
111
+ </span>
112
+ <% end %>
113
+ </div>
114
+ <div class="grid14">
115
+ <%= context.body %>
116
+ </div>
117
+ </article>
118
+ FORMAT
119
+ end
120
+
121
+ def page_format
122
+ <<-'FORMAT'
123
+ <!doctype html>
124
+ <body>
125
+ <section class="sidebar">
126
+ </section>
127
+ <section class="content">
128
+ <%= context %>
129
+ </section>
130
+ </body>
131
+ FORMAT
132
+ end
133
+
134
+ def landing_page_format
135
+ <<-'FORMAT'
136
+ <!doctype html>
137
+ <body class="land">
138
+ <section class="banner">
139
+ <img src="logo.png" />
140
+ <nav>
141
+ <ul>
142
+ <li><a href="recent.html">Posts</a></li>
143
+ <li><a href="projects.html">Projects</a></li>
144
+ </ul>
145
+ </nav>
146
+ </section>
147
+ <%= context[:most_recent].title %>
148
+ </body>
149
+ </html>
150
+ FORMAT
151
+ end
152
+
153
+ def default_post_params
154
+ {
155
+ title: random_string,
156
+ type: "Article",
157
+ tags: "Test, demo",
158
+ summary: "Teaser",
159
+ published: "now",
160
+ content: "Some repeated content.\n\n"*10
161
+ }
162
+ end
163
+
164
+ def create_post_with options={}
165
+ opts = default_post_params.merge(options)
166
+ s = []
167
+ s << opts.fetch(:title)
168
+ s << "Type: #{opts.fetch(:type)}"
169
+ s << "Tags: #{opts.fetch(:tags)}"
170
+ s << "Summary: #{opts.fetch(:summary)}"
171
+ s << "Published: #{opts.fetch(:published)}"
172
+ s << ""
173
+ s << opts.fetch(:content)
174
+ s.join "\n"
175
+ end
176
+
177
+ def save_post post, path
178
+ filename = post.split("\n").first.downcase.gsub(/ /, '-') + ".md"
179
+ write_file post, path + filename
180
+ end
181
+
182
+ def write_file content, path
183
+ File.open(path, "w:UTF-8"){|o| o.puts content}
184
+ end
185
+
186
+ def random_string length=8
187
+ (1..length).map{('a'..'z').to_a[rand(26)] }.join
188
+ end
@@ -0,0 +1,3 @@
1
+ After do
2
+ `rm -rf tmp`
3
+ end
@@ -0,0 +1,65 @@
1
+ require_relative '../contraption'
2
+
3
+ module Contraption
4
+ class Catalog
5
+ include Enumerable
6
+
7
+ attr_reader :items
8
+
9
+ def initialize items
10
+ @items = Array items
11
+ end
12
+
13
+ def << new_item
14
+ @items.concat(Array new_item)
15
+ end
16
+
17
+ def by_year
18
+ by_date { |date| Year.new date.year }
19
+ end
20
+
21
+ def by_month
22
+ by_date { |date| Month.new date.year, date.month }
23
+ end
24
+
25
+ def by_day
26
+ by_date { |date| Day.new date.year, date.month, date.day }
27
+ end
28
+
29
+ def by_tag
30
+ all(:tags).each_with_object(Hash.new {|h,k| h[k] = []}) do |tag, result|
31
+ items.each do |item|
32
+ result[tag.to_sym] << item if item.tags.include? tag
33
+ end
34
+ end
35
+ end
36
+
37
+ def each
38
+ items.each
39
+ end
40
+
41
+ def most_recent n=1
42
+ n = items.length if n < 0
43
+ items.sort_by{ |item| item.publication_date }
44
+ .reverse
45
+ .take n
46
+ end
47
+
48
+ private
49
+ def by &block
50
+ items.group_by do |item|
51
+ yield item
52
+ end
53
+ end
54
+
55
+ def by_date &block
56
+ by { |i| yield block.call(i.publication_date) }
57
+ end
58
+
59
+ def all attribute
60
+ items.map{|item| item.public_send(attribute.to_sym)}
61
+ .flatten
62
+ .uniq
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ require 'erb'
2
+
3
+ module Contraption
4
+ class Formatter
5
+ def initialize location
6
+ @source = location
7
+ find_formats
8
+ end
9
+
10
+ def format context, method=nil
11
+ renderer(method || context.type).result binding
12
+ end
13
+
14
+ def formats
15
+ @formats.keys
16
+ end
17
+
18
+ private
19
+ def find_formats
20
+ @formats = @source.list('.erb')
21
+ .inject({}){|collection, f| collection[f.split('.').first.to_sym] = f; collection}
22
+ end
23
+
24
+ def renderer type
25
+ ERB.new @source.read(
26
+ @formats.fetch(type){ raise ArgumentError, "#{type} is not a valid format" }
27
+ )
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,83 @@
1
+ require 'date'
2
+ require_relative 'tag'
3
+
4
+ module Contraption
5
+ class Header
6
+ class << self
7
+ def from string
8
+ Header.new (Hash[
9
+ string.lines
10
+ .map{|l| process l}
11
+ .reject{|l| l == nil}
12
+ ])
13
+ end
14
+
15
+ private
16
+ def process line
17
+ return nil if /^=+$/.match line
18
+ splitted = line.strip.split /^(\w+):\s+/, 2
19
+ return [:title, line.strip.chomp] if splitted.count == 1
20
+
21
+ key = splitted[1].downcase.to_sym
22
+ raw_value = splitted[2].strip
23
+ case key
24
+ when :external
25
+ [:external, raw_value]
26
+ when :filename
27
+ [:filename, raw_value]
28
+ when :published
29
+ [:publication_date, determine_date(raw_value)]
30
+ when :summary
31
+ [:summary, raw_value]
32
+ when :tags
33
+ [:tags, raw_value.split(/,\s+/).map{|t| Tag.new t}]
34
+ when :type
35
+ [:type, raw_value.downcase.to_sym]
36
+ when :title
37
+ [:title, raw_value]
38
+ else
39
+ nil
40
+ end
41
+ end
42
+
43
+ def determine_date raw_value
44
+ if raw_value.downcase == "now"
45
+ :now
46
+ else
47
+ DateTime.parse(raw_value)
48
+ end
49
+ end
50
+ end
51
+
52
+ def initialize opts={}
53
+ @defaults = { title: "",
54
+ publication_date: "",
55
+ summary: "",
56
+ type: "",
57
+ tags: []}.merge!(opts)
58
+ end
59
+
60
+ def filename ext='.md'
61
+ fn = @defaults.fetch(:filename) do
62
+ title.downcase
63
+ .gsub(/[^\w\s-]+/, ' ')
64
+ .strip
65
+ .gsub(/\s+/, '-') + ext
66
+ end.split ext
67
+ fn.join + ext
68
+ end
69
+
70
+ def update new_opts={}
71
+ Header.new @defaults.merge(new_opts)
72
+ end
73
+
74
+ def new?
75
+ publication_date == :now
76
+ end
77
+
78
+ private
79
+ def method_missing m, *a, &b
80
+ @defaults.fetch(m) { super }
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,11 @@
1
+ module Contraption
2
+ class HttpHandler
3
+ def protocol
4
+ :http
5
+ end
6
+
7
+ def handle request
8
+ request
9
+ end
10
+ end
11
+ end