contraption 0.2.0

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