shinmun 0.2 → 0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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