shinmun 0.2 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
data/test/blog_spec.rb ADDED
@@ -0,0 +1,156 @@
1
+ require 'shinmun'
2
+ require 'rack/mock'
3
+ require 'rexml/document'
4
+ require 'rexml/xpath'
5
+ require 'pp'
6
+
7
+ describe Shinmun::Blog do
8
+
9
+ DIR = '/tmp/shinmun-test'
10
+
11
+ attr_reader :blog
12
+
13
+ before do
14
+ ENV['RACK_ENV'] = 'production'
15
+
16
+ FileUtils.rm_rf DIR
17
+
18
+ Shinmun::Blog.init(DIR)
19
+
20
+ @blog = Shinmun::Blog.new(DIR)
21
+
22
+ blog.config = {
23
+ :title => 'Title',
24
+ :description => 'Description',
25
+ :language => 'en',
26
+ :author => 'The Author',
27
+ :categories => ['Ruby', 'Javascript']
28
+ }
29
+
30
+ @posts = [blog.create_post(:title => 'New post', :date => '2008-10-10', :category => 'Ruby', :body => 'Body1'),
31
+ blog.create_post(:title => 'And this', :date => '2008-10-11', :category => 'Ruby', :body => 'Body2'),
32
+ blog.create_post(:title => 'Again', :date => '2008-11-10', :category => 'Javascript', :body => 'Body3')]
33
+
34
+ @pages = [blog.create_page(:title => 'Page 1', :body => 'Body1'),
35
+ blog.create_page(:title => 'Page 2', :body => 'Body2')]
36
+
37
+ blog.store.load
38
+ end
39
+
40
+ def request(method, uri, options={})
41
+ @request = Rack::MockRequest.new(blog)
42
+ @response = @request.request(method, uri, options)
43
+ end
44
+
45
+ def get(*args)
46
+ request(:get, *args)
47
+ end
48
+
49
+ def post(*args)
50
+ request(:post, *args)
51
+ end
52
+
53
+ def xpath(xml, path)
54
+ REXML::XPath.match(REXML::Document.new(xml), path)
55
+ end
56
+
57
+ def query(hash)
58
+ Rack::Utils.build_query(hash)
59
+ end
60
+
61
+ def assert_listing(xml, list)
62
+ titles = xpath(xml, "//h2/a")
63
+ summaries = xpath(xml, "//p")
64
+
65
+ list.each_with_index do |(title, summary), i|
66
+ titles[i].text.should == title
67
+ summaries[i].text.to_s.strip.should == summary
68
+ end
69
+ end
70
+
71
+ it "should load templates" do
72
+ blog.load_template("index.rhtml").should be_kind_of(ERB)
73
+ end
74
+
75
+ it "should find posts for a category" do
76
+ category = blog.find_category('ruby')
77
+ category[:name].should == 'Ruby'
78
+
79
+ category[:posts].should include(@posts[0])
80
+ category[:posts].should include(@posts[1])
81
+
82
+ category = blog.find_category('javascript')
83
+ category[:name].should == 'Javascript'
84
+ category[:posts].should include(@posts[2])
85
+ end
86
+
87
+ it "should create a post" do
88
+ post = blog.create_post(:title => 'New post', :date => '2008-10-10')
89
+ blog.store.load
90
+
91
+ post = blog.find_post(2008, 10, 'new-post')
92
+ post.should_not be_nil
93
+ post.title.should == 'New post'
94
+ post.date.should == Date.new(2008, 10, 10)
95
+ post.name.should == 'new-post'
96
+ end
97
+
98
+ it "should render posts" do
99
+ xml = get('/2008/10/new-post').body
100
+
101
+ xpath(xml, "//h1")[0].text.should == 'New post'
102
+ xpath(xml, "//p")[0].text.should == 'Body1'
103
+ end
104
+
105
+ it "should render categories" do
106
+ get('/categories/ruby.rss')['Content-Type'].should == 'application/rss+xml'
107
+
108
+ xml = get('/categories/ruby.rss').body
109
+
110
+ xpath(xml, '/rss/channel/title')[0].text.should == 'Ruby'
111
+ xpath(xml, '/rss/channel/item/title')[0].text.should == 'And this'
112
+ xpath(xml, '/rss/channel/item/pubDate')[0].text.should == "Sat, 11 Oct 2008 00:00:00 +0000"
113
+ xpath(xml, '/rss/channel/item/link')[0].text.should == "http://example.org/2008/10/and-this"
114
+ xpath(xml, '/rss/channel/item/title')[1].text.should == 'New post'
115
+ xpath(xml, '/rss/channel/item/pubDate')[1].text.should == "Fri, 10 Oct 2008 00:00:00 +0000"
116
+ xpath(xml, '/rss/channel/item/link')[1].text.should == "http://example.org/2008/10/new-post"
117
+
118
+ assert_listing(get('/categories/ruby').body, [['And this', 'Body2'], ['New post', 'Body1']])
119
+ end
120
+
121
+ it "should render index and archives" do
122
+ blog.posts_for_month(2008, 10).should_not be_empty
123
+ blog.posts_for_month(2008, 11).should_not be_empty
124
+
125
+ assert_listing(get('/2008/10').body, [['And this', 'Body2'], ['New post', 'Body1']])
126
+ assert_listing(get('/').body, [['Again', 'Body3'], ['And this', 'Body2'], ['New post', 'Body1']])
127
+ end
128
+
129
+ it "should render pages" do
130
+ xml = get('/page-1').body
131
+ xpath(xml, "//h1")[0].text.should == 'Page 1'
132
+ xpath(xml, "//p")[0].text.should == 'Body1'
133
+
134
+ xml = get('/page-2').body
135
+ xpath(xml, "//h1")[0].text.should == 'Page 2'
136
+ xpath(xml, "//p")[0].text.should == 'Body2'
137
+ end
138
+
139
+ it "should post a comment" do
140
+ post "/2008/10/new-post/comments?name=Hans&text=Hallo"
141
+ post "/2008/10/new-post/comments?name=Peter&text=Servus"
142
+
143
+ blog.store.load
144
+
145
+ comments = blog.comments_for(@posts[0])
146
+
147
+ comments[0].should_not be_nil
148
+ comments[0].name.should == 'Hans'
149
+ comments[0].text.should == 'Hallo'
150
+
151
+ comments[1].should_not be_nil
152
+ comments[1].name.should == 'Peter'
153
+ comments[1].text.should == 'Servus'
154
+ end
155
+
156
+ end
data/test/post_spec.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'shinmun'
2
+
3
+ describe Shinmun::Post do
4
+
5
+ POST = <<-END
6
+ ---
7
+ category: Javascript
8
+ date: 2008-09-09
9
+ tags: template, engine, json
10
+ title: Patroon - a Javascript Template Engine
11
+ ---
12
+ Patroon is a template engine written in Javascript in about 100 lines
13
+ of code. It takes existing DOM nodes annotated with CSS classes and
14
+ expand a data object according to simple rules. Additionally you may
15
+ use traditional string interpolation inside attribute values and text
16
+ nodes.
17
+ END
18
+
19
+ it 'should parse and dump in the same way' do
20
+ Shinmun::Post.new(:type => 'md', :src => POST).dump.should == (POST)
21
+ end
22
+
23
+ it "should parse the yaml header" do
24
+ post = Shinmun::Post.new(:type => 'md', :src => POST)
25
+ post.title.should == 'Patroon - a Javascript Template Engine'
26
+ post.category.should == 'Javascript'
27
+ post.date.should == Date.new(2008,9,9)
28
+ post.tags.should == 'template, engine, json'
29
+ post.tag_list.should == ['template', 'engine', 'json']
30
+ end
31
+
32
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shinmun
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.2"
4
+ version: "0.5"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Georgi
@@ -9,11 +9,12 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-11-17 00:00:00 +01:00
12
+ date: 2010-02-07 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: BlueCloth
17
+ type: :runtime
17
18
  version_requirement:
18
19
  version_requirements: !ruby/object:Gem::Requirement
19
20
  requirements:
@@ -23,6 +24,7 @@ dependencies:
23
24
  version:
24
25
  - !ruby/object:Gem::Dependency
25
26
  name: rubypants
27
+ type: :runtime
26
28
  version_requirement:
27
29
  version_requirements: !ruby/object:Gem::Requirement
28
30
  requirements:
@@ -30,7 +32,27 @@ dependencies:
30
32
  - !ruby/object:Gem::Version
31
33
  version: "0"
32
34
  version:
33
- description: Shinmun is a blog engine, which renders text files using a markup language like Markdown and a set of templates into either static web pages or serving them over a rack adapter. Shinmun supports categories, archives, rss feeds and commenting.
35
+ - !ruby/object:Gem::Dependency
36
+ name: rack
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: coderay
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ description: git-based blog engine.
34
56
  email: matti.georgi@gmail.com
35
57
  executables:
36
58
  - shinmun
@@ -39,24 +61,37 @@ extensions: []
39
61
  extra_rdoc_files:
40
62
  - README.md
41
63
  files:
42
- - .gitignore
43
- - LICENSE
44
64
  - README.md
45
65
  - Rakefile
66
+ - assets/print.css
67
+ - assets/styles.css
46
68
  - bin/shinmun
69
+ - config.ru
47
70
  - lib/shinmun.rb
48
- - lib/shinmun/admin_controller.rb
49
- - lib/shinmun/aggregations/delicious.rb
50
- - lib/shinmun/aggregations/flickr.rb
51
71
  - lib/shinmun/blog.rb
52
- - lib/shinmun/cache.rb
72
+ - lib/shinmun/bluecloth_coderay.rb
53
73
  - lib/shinmun/comment.rb
54
- - lib/shinmun/controller.rb
74
+ - lib/shinmun/handlers.rb
55
75
  - lib/shinmun/helpers.rb
56
76
  - lib/shinmun/post.rb
57
- - lib/shinmun/template.rb
77
+ - lib/shinmun/routes.rb
78
+ - templates/index.rhtml
79
+ - templates/page.rhtml
80
+ - templates/404.rhtml
81
+ - templates/_comments.rhtml
82
+ - templates/category.rhtml
83
+ - templates/_comment_form.rhtml
84
+ - templates/post.rhtml
85
+ - templates/index.rxml
86
+ - templates/category.rxml
87
+ - templates/archive.rhtml
88
+ - templates/layout.rhtml
89
+ - test/blog_spec.rb
90
+ - test/post_spec.rb
58
91
  has_rdoc: true
59
- homepage: http://shinmun.rubyforge.org
92
+ homepage: http://github.com/georgi/shinmun
93
+ licenses: []
94
+
60
95
  post_install_message:
61
96
  rdoc_options: []
62
97
 
@@ -76,10 +111,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
111
  version:
77
112
  requirements: []
78
113
 
79
- rubyforge_project: shinmun
80
- rubygems_version: 1.1.1
114
+ rubyforge_project:
115
+ rubygems_version: 1.3.5
81
116
  signing_key:
82
- specification_version: 2
83
- summary: a small blog engine
117
+ specification_version: 3
118
+ summary: git-based blog engine
84
119
  test_files: []
85
120
 
data/.gitignore DELETED
@@ -1,3 +0,0 @@
1
- *~
2
- doc/*
3
- pkg/*
data/LICENSE DELETED
@@ -1,18 +0,0 @@
1
- Copyright (c) 2008 Matthias Georgi <http://www.matthias-georgi.de>
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining a copy
4
- of this software and associated documentation files (the "Software"), to
5
- deal in the Software without restriction, including without limitation the
6
- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
- sell copies of the Software, and to permit persons to whom the Software is
8
- furnished to do so, subject to the following conditions:
9
-
10
- The above copyright notice and this permission notice shall be included in
11
- all copies or substantial portions of the Software.
12
-
13
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
- THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,161 +0,0 @@
1
- require 'rack'
2
- require 'json'
3
-
4
- module Shinmun
5
-
6
- class AdminController
7
-
8
- def initialize(blog)
9
- @blog = blog
10
- end
11
-
12
- def tree(request)
13
- file_node(request.path_info[1..-1], 1).to_json
14
- end
15
-
16
- def get_page(request, path)
17
- page = @blog.find_page(path)
18
-
19
- { :title => page.title,
20
- :date => page.date,
21
- :category => page.category,
22
- :tags => page.tags ? page.tags.join(',') : nil,
23
- :body => page.body }.to_json
24
- end
25
-
26
- def put_page(request, path)
27
- params = request.params
28
- page = @blog.find_page(path)
29
-
30
- page.title = params['title']
31
- page.author = params['author']
32
- page.date = Date.parse(params['date']) rescue nil
33
- page.category = params['category']
34
- page.tags = params['tags']
35
- page.languages = params['languages']
36
- page.body = params['body']
37
- page.save
38
-
39
- git_add(page.filename, 'changed')
40
-
41
- return ''
42
- end
43
-
44
- def get_file(request, path)
45
- File.read(path)
46
- end
47
-
48
- def put_file(request, path)
49
- File.open(path, 'w') do |io|
50
- io << request.params['body']
51
- end
52
- git_add(path, 'changed')
53
- return ''
54
- end
55
-
56
- def data(request)
57
- path = request.path_info[1..-1]
58
- match = path.match(/^posts\/(.*)\.(.*)$/)
59
- method = request.request_method.downcase
60
-
61
- if match
62
- send("#{method}_page", request, match[1])
63
- else
64
- send("#{method}_file", request, path)
65
- end
66
- end
67
-
68
- def new_folder(request)
69
- path = request.path_info[1..-1] + '/' + request.params['name']
70
-
71
- unless File.exist?(path)
72
- Dir.mkdir(path)
73
- end
74
-
75
- return ''
76
- end
77
-
78
- def new_file(request)
79
- path = request.path_info[1..-1] + '/' + request.params['name']
80
-
81
- unless File.exist?(path)
82
- File.open(path, "w").close
83
- git_add(path, 'created')
84
- end
85
-
86
- return ''
87
- end
88
-
89
- def rename(request)
90
- path = request.path_info[1..-1]
91
- dest = File.basename(path) + '/' + request.params['name']
92
-
93
- if File.exist?(path) and !File.exist?(dest)
94
- `git mv #{path} #{dest}`
95
- `git commit -m 'moved #{path} to #{dest}'`
96
- end
97
-
98
- return ''
99
- end
100
-
101
- def delete(request)
102
- path = request.path_info[1..-1]
103
-
104
- if File.file?(path)
105
- `git rm #{path}`
106
- `git commit -m 'deleted #{path}'`
107
- end
108
-
109
- return ''
110
- end
111
-
112
- def call(env)
113
- request = Rack::Request.new(env)
114
- response = Rack::Response.new
115
- action = request.params['action']
116
-
117
- response.body = send(action, request) if self.class.public_instance_methods.include?(action)
118
-
119
- response.status = 200
120
- response.finish
121
- end
122
-
123
- protected
124
-
125
- def git_add(file, message)
126
- `git add #{file}`
127
- `git commit -m '#{message} #{file}'`
128
- end
129
-
130
- def entries_for(path)
131
- Dir.entries(path).reject { |f| f.match /(\.|~)$/ }.sort
132
- end
133
-
134
- def root
135
- { :children => ['config', 'posts', 'public', 'templates'].map { |f| file_node(f, 1) } }
136
- end
137
-
138
- def file_node(path, depth)
139
- return root if path.empty?
140
-
141
- stat = File.stat(path)
142
-
143
- hash = {
144
- :id => path,
145
- :cls => stat.file? ? 'file' : 'folder',
146
- :text => File.basename(path),
147
- :leaf => stat.file?
148
- }
149
-
150
- unless stat.file?
151
- hash[:children] = entries_for(path).map do |entry|
152
- file_node(File.join(path, entry), depth - 1)
153
- end
154
- end
155
-
156
- hash
157
- end
158
-
159
- end
160
-
161
- end
@@ -1,57 +0,0 @@
1
- require 'open-uri'
2
- require 'time'
3
- require 'rexml/document'
4
-
5
- class Delicious
6
- include REXML
7
-
8
- attr_accessor :url, :items, :link, :title, :days
9
-
10
- # This object holds given information of an item
11
- class DeliciousItem < Struct.new(:link, :title, :description, :description_link, :date)
12
- def to_s; title end
13
- end
14
-
15
- # Pass the url to the RSS feed you would like to keep tabs on
16
- # by default this will request the rss from the server right away and
17
- # fill the items array
18
- def initialize(url, refresh = true)
19
- self.items = []
20
- self.url = url
21
- self.days = {}
22
- self.refresh if refresh
23
- end
24
-
25
- # This method lets you refresh the items in the items array
26
- # useful if you keep the object cached in memory and
27
- def refresh
28
- open(@url) do |http|
29
- parse(http.read)
30
- end
31
- end
32
-
33
- private
34
-
35
- def parse(body)
36
-
37
- xml = Document.new(body)
38
-
39
- self.items = []
40
- self.link = XPath.match(xml, "//channel/link/text()").first.value rescue ""
41
- self.title = XPath.match(xml, "//channel/title/text()").first.value rescue ""
42
-
43
- XPath.each(xml, "//item/") do |elem|
44
- item = DeliciousItem.new
45
- item.title = XPath.match(elem, "title/text()").first.value rescue ""
46
- item.link = XPath.match(elem, "link/text()").first.value rescue ""
47
- item.description = XPath.match(elem, "description/text()").first.value rescue ""
48
- item.date = Time.mktime(*ParseDate.parsedate(XPath.match(elem, "dc:date/text()").first.value)) rescue Time.now
49
-
50
- item.description_link = item.description
51
- item.description.gsub!(/<\/?a\b.*?>/, "") # remove all <a> tags
52
- items << item
53
- end
54
-
55
- self.items = items.sort_by { |item| item.date }.reverse
56
- end
57
- end
@@ -1,81 +0,0 @@
1
- require 'open-uri'
2
- require 'time'
3
- require 'rexml/document'
4
-
5
- # Example:
6
- #
7
- # flickr = Flickr.new('http://www.flickr.com/services/feeds/photos_public.gne?id=40235412@N00&format=rss_200')
8
- # flickr.pics.each do |pic|
9
- # puts "#{pic.title} @ #{pic.link} updated at #{pic.date}"
10
- # end
11
- #
12
- class FlickrAggregation
13
- include REXML
14
-
15
- def choose(num)
16
- return pics unless pics.size > num
17
- bag = []
18
- set = pics.dup
19
- num.times {|x| bag << set.delete_at(rand(set.size))}
20
- bag
21
- end
22
-
23
- attr_accessor :url, :pics, :link, :title, :description
24
-
25
- # This object holds given information of a picture
26
- class Picture
27
- attr_accessor :link, :title, :date, :description, :thumbnail
28
-
29
- def to_s
30
- title
31
- end
32
-
33
- def date=(value)
34
- @date = Time.parse(value)
35
- end
36
-
37
- end
38
-
39
- # Pass the url to the RSS feed you would like to keep tabs on
40
- # by default this will request the rss from the server right away and
41
- # fill the tasks array
42
- def initialize(url, refresh = true)
43
- self.pics = []
44
- self.url = url
45
- self.refresh if refresh
46
- end
47
-
48
- # This method lets you refresh the tasks int the tasks array
49
- # useful if you keep the object cached in memory and
50
- def refresh
51
- open(@url) do |http|
52
- parse(http.read)
53
- end
54
- end
55
-
56
- private
57
-
58
- def parse(body)
59
-
60
- xml = Document.new(body)
61
-
62
- self.pics = []
63
- self.link = XPath.match(xml, "//channel/link/text()").to_s
64
- self.title = XPath.match(xml, "//channel/title/text()").to_s
65
- self.description = XPath.match(xml, "//channel/description/text()").to_s
66
-
67
- XPath.each(xml, "//item/") do |elem|
68
-
69
- picture = Picture.new
70
- picture.title = XPath.match(elem, "title/text()").to_s
71
- picture.date = XPath.match(elem, "pubDate/text()").to_s
72
- picture.link = XPath.match(elem, "link/text()").to_s
73
- picture.description = XPath.match(elem, "description/text()").to_s
74
- picture.thumbnail = XPath.match(elem, "media:thumbnail/@url").to_s
75
-
76
- pics << picture
77
- end
78
- end
79
- end
80
-
81
-
data/lib/shinmun/cache.rb DELETED
@@ -1,59 +0,0 @@
1
- module Shinmun
2
-
3
- # A simple hashtable, which loads only changed files by calling reload.
4
- class Cache
5
-
6
- # Call with a block to specify how the data is loaded.
7
- # This is the default behaviour: Cache.new {|file| File.read(file) }
8
- def initialize(&block)
9
- @map = {}
10
- @callback = block || proc { |file| File.read(file) }
11
- end
12
-
13
- # Load a file into the cache, transform it according to callback
14
- # and remember the modification time.
15
- def load(file)
16
- data = @callback.call(file)
17
- @map[file] = [data, File.mtime(file)]
18
- data
19
- end
20
-
21
- def remove(file)
22
- @map.delete(file)
23
- end
24
-
25
- def dirty_files
26
- @map.map { |file, (data, mtime)| mtime != File.mtime(file) ? file : nil }.compact
27
- end
28
-
29
- def reload!
30
- @map.keys.each { |file| load file }
31
- end
32
-
33
- def reload_dirty!
34
- dirty_files.each { |file| load file }
35
- end
36
-
37
- # Access the cache by filename.
38
- def [](file)
39
- data, mtime = @map[file]
40
- data or load(file)
41
- end
42
-
43
- def values
44
- @map.values.map { |data, | data }
45
- end
46
-
47
- # Are there any files loaded?
48
- def empty?
49
- @map.empty?
50
- end
51
-
52
- # Is there any file in this cache, which has changed?
53
- def dirty?
54
- dirty_files.size > 0
55
- end
56
-
57
- end
58
-
59
- end