PlainSite 1.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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +43 -0
  4. data/LICENSE +20 -0
  5. data/PlainSite.gemspec +40 -0
  6. data/README.md +276 -0
  7. data/Rakefile +34 -0
  8. data/_config.yml +6 -0
  9. data/bin/plainsite +76 -0
  10. data/lib/PlainSite.rb +5 -0
  11. data/lib/PlainSite/Commands.rb +76 -0
  12. data/lib/PlainSite/Data/Category.rb +235 -0
  13. data/lib/PlainSite/Data/FrontMatterFile.rb +64 -0
  14. data/lib/PlainSite/Data/Post.rb +237 -0
  15. data/lib/PlainSite/Data/PostList.rb +164 -0
  16. data/lib/PlainSite/Data/PostListPage.rb +80 -0
  17. data/lib/PlainSite/RenderTask.rb +235 -0
  18. data/lib/PlainSite/Site.rb +330 -0
  19. data/lib/PlainSite/SocketPatch.rb +15 -0
  20. data/lib/PlainSite/Tpl/ExtMethods.rb +55 -0
  21. data/lib/PlainSite/Tpl/LayErb.rb +73 -0
  22. data/lib/PlainSite/Utils.rb +79 -0
  23. data/lib/PlainSite/_scaffold/_src/assets/README.md +5 -0
  24. data/lib/PlainSite/_scaffold/_src/assets/css/style.css +506 -0
  25. data/lib/PlainSite/_scaffold/_src/assets/favicon.ico +0 -0
  26. data/lib/PlainSite/_scaffold/_src/config.yml +10 -0
  27. data/lib/PlainSite/_scaffold/_src/data/essays/game-of-life.md +15 -0
  28. data/lib/PlainSite/_scaffold/_src/data/essays/phoenix-rebirth.html +15 -0
  29. data/lib/PlainSite/_scaffold/_src/data/programming/hello-world.md +48 -0
  30. data/lib/PlainSite/_scaffold/_src/extensions/TplExt.rb +23 -0
  31. data/lib/PlainSite/_scaffold/_src/routes.rb +49 -0
  32. data/lib/PlainSite/_scaffold/_src/templates/404.html +16 -0
  33. data/lib/PlainSite/_scaffold/_src/templates/about.html +11 -0
  34. data/lib/PlainSite/_scaffold/_src/templates/base.html +32 -0
  35. data/lib/PlainSite/_scaffold/_src/templates/header.html +8 -0
  36. data/lib/PlainSite/_scaffold/_src/templates/index.html +25 -0
  37. data/lib/PlainSite/_scaffold/_src/templates/list.html +41 -0
  38. data/lib/PlainSite/_scaffold/_src/templates/post.html +81 -0
  39. data/lib/PlainSite/_scaffold/_src/templates/rss.erb +29 -0
  40. data/test/CategoryTest.rb +63 -0
  41. data/test/FrontMatterFileTest.rb +40 -0
  42. data/test/LayErbTest.rb +20 -0
  43. data/test/ObjectProxyTest.rb +30 -0
  44. data/test/PostListTest.rb +55 -0
  45. data/test/PostTest.rb +48 -0
  46. data/test/SiteTest.rb +105 -0
  47. data/test/fixtures/2012-06-12-test.md +7 -0
  48. data/test/fixtures/category-demo/2012-06-12-post1.md +7 -0
  49. data/test/fixtures/category-demo/2013-05-01-post2.md +7 -0
  50. data/test/fixtures/category-demo/_meta.yml +1 -0
  51. data/test/fixtures/category-demo/index.md +6 -0
  52. data/test/fixtures/category-demo/sub-category1/sub-post1.md +1 -0
  53. data/test/fixtures/category-demo/sub-category2/sub-post2.md +1 -0
  54. data/test/fixtures/include.erb +1 -0
  55. data/test/fixtures/invalid-front-matter.html +7 -0
  56. data/test/fixtures/layout.erb +1 -0
  57. data/test/fixtures/no-front-matter.html +2 -0
  58. data/test/fixtures/tpl.erb +7 -0
  59. data/test/fixtures/valid-front-matter.html +14 -0
  60. data/test/runtest +6 -0
  61. metadata +202 -0
@@ -0,0 +1,164 @@
1
+ #coding:utf-8
2
+ module PlainSite;end
3
+ module PlainSite::Data
4
+ require 'PlainSite/Data/Post'
5
+ require 'PlainSite/Data/PostListPage'
6
+ class PostList
7
+ include Enumerable
8
+ # PostList,default sort by date desc and by name asc
9
+ # posts - The Post[]|String[] of posts or posts file path(abs or relative to site.data_path) array
10
+ # site - The Site
11
+ def initialize(posts,site)
12
+ if String===posts[0]
13
+ @posts=posts.map {|f| Post.new f,site }
14
+ else
15
+ @posts=posts.map &:dup
16
+ end
17
+ @posts.sort! do |a,b|
18
+ # sort by date desc and by name asc
19
+ [b.date,a.slug] <=> [a.date,b.slug]
20
+ end
21
+ @site=site
22
+ @custom_index= @posts.any? &:is_index
23
+ end
24
+
25
+ # Array like slice method
26
+ # Return the Post or slice PostList
27
+ def [](*args)
28
+ if args.length==1
29
+ i=args[0]
30
+ if Range===i
31
+ posts=@posts[i]
32
+ return nil if posts.nil? || posts.empty?
33
+ PostList.new posts,@site
34
+ else
35
+ post=@posts[i]
36
+ if post
37
+ post.next_post= i-1 < 0 ? nil : @posts[i-1] # Because -1 will index from last
38
+ post.prev_post= @posts[i+1]
39
+ end
40
+ post
41
+ end
42
+ else
43
+ start,len=args
44
+ posts=@posts[start,len]
45
+ return nil if posts.nil? || posts.empty?
46
+ PostList.new posts,@site
47
+ end
48
+ end
49
+ alias :slice :[]
50
+
51
+ # Paginate post list
52
+ # This is a smart paginater. Version Control System friendly!
53
+ # What's VCS friendly? It's page nums use a revert order.
54
+ # The old the page's date is,the smaller the page's num is.
55
+ # Options:
56
+ # page_size - The Integer page size number,must be more than zero,default is 10
57
+ # revert_nos - The Boolean value to control if use revert order page num,default is true
58
+ # Return: The PostListPage[]
59
+ def paginate(opts={})
60
+ revert_nos=opts[:revert_nos].nil? ? true : opts[:revert_nos]
61
+ page_size=opts[:page_size] || 10
62
+ total=@posts.length
63
+ return [] if total==0
64
+
65
+ # In revert nos case,the first page need padding to fit full page
66
+ if revert_nos && total>page_size
67
+ start=total % page_size
68
+ pages=[self.slice(0,page_size)]
69
+ else
70
+ start=0
71
+ pages=[]
72
+ end
73
+
74
+ while posts=self.slice(start,page_size)
75
+ pages.push posts
76
+ start+=page_size
77
+ end
78
+
79
+ nos_list=(1..pages.length).to_a
80
+ display_nums=nos_list.dup
81
+ slugs=('a'..'zzz').take pages.length # Use letters id instead of numbers
82
+
83
+ if revert_nos
84
+ nos_list.reverse!
85
+ slugs.reverse!
86
+ end
87
+ slugs[0]='index' unless @custom_index # Category has its custom index post
88
+
89
+ total_pages_count=pages.length
90
+ pages= pages.zip(nos_list,display_nums,slugs).map do |a|
91
+ posts,num,display_num,slug=a
92
+ PostListPage.new(
93
+ # num: num, # It's useles
94
+ slug: slug, display_num: display_num,
95
+ posts: posts, site: @site,
96
+ total_pages_count: total_pages_count,
97
+ total_posts_count: total,
98
+ page_size: page_size,
99
+ revert_nos: revert_nos
100
+ )
101
+ end
102
+
103
+ next_pages=[nil]+pages[0..-2]
104
+ prev_pages=(pages[1..-1] or [])+[nil]
105
+ pages.zip(prev_pages,next_pages) do |a|
106
+ page,prev_page,next_page=a
107
+ page.prev_page=prev_page
108
+ page.next_page=next_page
109
+ page.all_pages=pages
110
+ end
111
+
112
+ pages
113
+ end
114
+
115
+ # Install B
116
+ # `posts / 5` is same as `posts.paginate(page_size:5)`
117
+ def /(page_size)
118
+ paginate(page_size:page_size)
119
+ end
120
+
121
+ def +(other)
122
+ raise TypeError,"Except #{PostList} Type" unless PostList===other
123
+ PostList.new @posts+other.to_a
124
+ end
125
+
126
+ # Check if contains one post
127
+ # p - The Post object or the String path or relpath or data_id of post
128
+ def include?(p)
129
+ if Post===p
130
+ return @posts.include? p
131
+ end
132
+ return @posts.any? {|post| post.path==p || post.relpath==p || post.data_id==p}
133
+ end
134
+
135
+ def length
136
+ @posts.length
137
+ end
138
+
139
+ def empty?
140
+ length==0
141
+ end
142
+
143
+ def each(&block)
144
+ block_given? or return enum_for __method__
145
+ 0.upto @posts.length-1 do |i|
146
+ yield self[i]
147
+ end
148
+ end
149
+
150
+ def to_a
151
+ @posts.dup
152
+ end
153
+ alias :to_ary :to_a
154
+
155
+ %w(drop drop_while find_all select reject sort sort_by take take_while).each do |method|
156
+ define_method(method) do |*args,&block|
157
+ ary=super *args,&block
158
+ if ary
159
+ PostList.new ary.to_a,@site
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,80 @@
1
+ #coding:utf-8
2
+ module PlainSite;end
3
+ module PlainSite::Data
4
+ require 'ostruct'
5
+ # PostList Pagination
6
+ class PostListPage<OpenStruct
7
+ # These attributes' accessors are auto generated by OpenStruct
8
+ # attr_reader(
9
+ # :slug,:display_num,
10
+ # :posts,:prev_page,:next_page,
11
+ # :total_pages_count,:total_posts_count,:page_size,:all_pages,
12
+ # :revert_nos,:site
13
+ # )
14
+ attr_accessor :prev_page,:next_page,:all_pages # set after create
15
+
16
+ # Options:
17
+ # slug - The String page name suggested to used in url,first page is 'index',others are 'a'...'zzz' string
18
+ # display_num - The Integer always be ascending,first page is 1,second page is 2
19
+ # posts - The PostList
20
+ # prev_page - The previous PostListPage
21
+ # next_page - The next PostListPage
22
+ # total_pages_count - The Integer of total count in this pagination
23
+ # total_posts_count - The Integer of total count in this pagination
24
+ # page_size - The Integer of preset page size
25
+ # revert_nos - The Boolean indicate if this post list page use revert nos.
26
+ # When false case,it's html page must be regenerated every time.
27
+ # site - The Site belongs to
28
+ def initialize(opts)
29
+ super opts
30
+ end
31
+
32
+ # The String url of self
33
+ def url
34
+ site.url_for self
35
+ end
36
+
37
+ # See PostList#include?
38
+ def include?(a)
39
+ posts.include? a
40
+ end
41
+
42
+
43
+ # @deprecated
44
+
45
+ # Remove posts,resort all previous pages
46
+ # list - The String[] of post's path list
47
+ def remove(list)
48
+ posts=posts.select do |p|
49
+ list.include? p.path
50
+ end
51
+ try_pad_from_other_page
52
+ end
53
+
54
+
55
+ # Pop posts
56
+ # Return Post[]
57
+ def pop_multi(n)
58
+ ret=posts.slice(-n,n)
59
+ posts=posts.slice(0,-n-1)
60
+ ret
61
+ end
62
+
63
+ def try_pad_from_other_page
64
+ n=page_size-posts.length
65
+ return [] unless n>0
66
+ related_pages=[self]
67
+ if prev_page
68
+ posts=prev_page.posts.pop_multi(n)+posts
69
+ related_pages.concat prev_page.try_pad_from_other_page
70
+ elsif next_page
71
+ posts+=next_page.posts.take n
72
+ end
73
+ related_pages
74
+ end
75
+
76
+
77
+
78
+
79
+ end
80
+ end
@@ -0,0 +1,235 @@
1
+ #coding:utf-8
2
+
3
+ module PlainSite
4
+ require 'fileutils'
5
+ require 'pathname'
6
+ require 'PlainSite/Data/PostList'
7
+ require 'PlainSite/Data/PostListPage'
8
+ require 'PlainSite/Data/Post'
9
+ require 'PlainSite/Tpl/LayErb'
10
+ require 'PlainSite/Utils'
11
+
12
+ class BadUrlPatternException<Exception;end
13
+ class RenderTask
14
+ Post=Data::Post
15
+ PostList=Data::PostList
16
+ PostListPage=Data::PostListPage
17
+ attr_reader :site
18
+ # site - The Site
19
+ def initialize(site)
20
+ @site=site
21
+ @tasks=[]
22
+ end
23
+
24
+ # Options:
25
+ # url_pattern - The String with var replacement pattern "{property.property}"
26
+ # "{property.property}" is the data Hash key path.
27
+ # Example:
28
+ # "/article/{date.year}/{name}.html" will render url
29
+ # "/article/2011/hello-world.html"
30
+ # with post "posts/2011-09-09-hello-world.md"
31
+ # data - The Array|PostList|Object|String data to render
32
+ # In String case,it represents the post.data_id or category.data_id.
33
+ # Example: 'essay/*' same as $site.data['essay/*']
34
+ # If `data` type is Array or PostList,it will render each item with template,
35
+ # else only generate one page
36
+ # template - The String template path relative to '_site/templates'
37
+ # build_anyway - The Boolean value to indicate
38
+ # this route rule will build anyway even if no post updates
39
+ def route(opts)
40
+ url_pattern=opts[:url_pattern]
41
+ items=opts[:data]
42
+ template=opts[:template]
43
+ build_anyway=opts[:build_anyway]
44
+
45
+ if String===items
46
+ items=@site.data[items]
47
+ raise Exception,"Data not found:#{opts[:data]}!" if items.nil?
48
+ end
49
+ items=[items] unless Data::PostList===items || Array===items
50
+
51
+ tasks= items.map do |item|
52
+ urlpath=RenderTask.sub_url url_pattern,item
53
+ urlpath[0]='' if urlpath[0]=='/'
54
+ id =if item.respond_to? :data_id
55
+ item.data_id
56
+ else
57
+ item.object_id
58
+ end
59
+
60
+ { id: id, urlpath: urlpath, item: item,
61
+ template:File.join(@site.templates_path,template),
62
+ build_anyway:build_anyway }
63
+ end
64
+ @tasks.concat tasks
65
+ end
66
+
67
+ # Get the url for object
68
+ # obj - The Post|PageListPage|Category|String
69
+ # Return the String url prefix with site root url(site.url) or relative path if build --local
70
+ def url_for(obj)
71
+ obj[0]='' if String===obj && obj[0]=='/'
72
+
73
+ if String===obj && (File.exists? (File.join @site.assets_path,obj))
74
+ # static file path
75
+ urlpath=obj
76
+ else
77
+ urlpath=object2url obj
78
+ end
79
+
80
+ if @site.local
81
+ urlpath=urlpath+'/index.html' if urlpath.end_with? '/'
82
+ urlpath='index.html' if urlpath.empty?
83
+ p1=Pathname.new (File.dirname urlpath)
84
+ basename=File.basename urlpath
85
+ (p1.relative_path_from Pathname.new(@site._cur_page_dir)).to_s+'/'+basename
86
+ else
87
+ #URI.join @site.url,urlpath
88
+ '/' + urlpath
89
+ end
90
+ end
91
+
92
+ def object2url(obj)
93
+ id =if String===obj
94
+ obj
95
+ elsif obj.respond_to? :data_id
96
+ obj.data_id
97
+ else
98
+ obj.object_id
99
+ end
100
+ return (id2url_map[id] || id).to_s
101
+ end
102
+
103
+ # Return all valid output pages url path
104
+ def all_urlpath
105
+ return @all_urlpath if @all_urlpath
106
+ @all_urlpath=@tasks.map do |t|
107
+ t[:urlpath]
108
+ end
109
+ end
110
+
111
+ # Render pages
112
+ # partials - The Hash of new or updated and deleted posts and templates.
113
+ # If nil,it will render all pages.
114
+ # Structure:
115
+ # {
116
+ # updated_posts:[],
117
+ # updated_templates:[],
118
+ # has_deleted_posts:Bool
119
+ # }
120
+ def render(partials=nil)
121
+ if partials.nil?
122
+ return @tasks.each {|t|render_task t}
123
+ end
124
+ build_tasks,other_tasks=@tasks.partition {|t|!!t[:build_anyway]}
125
+
126
+ if tpls=partials[:updated_templates]
127
+ a,other_tasks=other_tasks.partition {|t|tpls.include? t[:template] }
128
+ build_tasks.concat a
129
+ end
130
+ if posts=partials[:updated_posts]
131
+ a,other_tasks=other_tasks.partition do |t|
132
+ _detectContainsPosts(t[:item],posts)
133
+ end
134
+ build_tasks.concat a
135
+ end
136
+
137
+ if partials[:has_deleted_posts]
138
+ # rebuild all post list pages
139
+ a,other_tasks=other_tasks.partition {|t|PostList===t[:item] || PostListPage===t[:item]}
140
+ build_tasks.concat a
141
+ end
142
+ build_tasks.each do |t|
143
+ render_task t
144
+ end
145
+ end
146
+
147
+ # Render single url corresponding task
148
+ # url - The String url pathname one can be used in browser,such as
149
+ # '/posts','/posts/','/posts/index.html' are both valid
150
+ # Returns the String page content
151
+ def render_url(url)
152
+ url=url.dup
153
+ url[0]='' if url[0]=='/'
154
+ url=url+'index.html' if url.end_with? '/'
155
+ # url=url+'/index.html' if site.url.start_with? url
156
+ t = @tasks.detect {|t|t[:urlpath]==url}
157
+ if t
158
+ return render_task t,false
159
+ end
160
+ end
161
+
162
+ private
163
+ # The Hash of data_id=>url or object_id=>url
164
+ def id2url_map
165
+ return @id2url_map if @id2url_map
166
+ @id2url_map=@tasks.group_by {|a| a[:id]}
167
+ @id2url_map.merge!(@id2url_map) do |k,v|
168
+ if v.length>1
169
+ urls=(v.map {|a|a[:urlpath]}).join "\n\t"
170
+ $stderr.puts "Object[#{k}] has more than one url:"
171
+ $stderr.puts "\t#{urls}"
172
+ end
173
+ v[0][:urlpath]
174
+ end
175
+ @id2url_map
176
+ end
177
+
178
+ def render_task(t,write_file=true)
179
+ item=t[:item]
180
+ urlpath=t[:urlpath]
181
+ template=t[:template]
182
+
183
+ @site._cur_page_dir = File.dirname(urlpath) if @site.local
184
+
185
+ item=Utils::ObjectProxy.new item,{site:@site} # Keep site always accessable
186
+ erb=Tpl::LayErb.new template
187
+ result=erb.render item
188
+
189
+ return result unless write_file
190
+ output_path=File.join(@site.dest,urlpath)
191
+ dir=File.dirname output_path
192
+ FileUtils.mkdir_p dir
193
+ File.open(output_path,'wb') do |f|
194
+ f.write result
195
+ end
196
+ end
197
+
198
+ # Render url_pattern with context data
199
+ # url_pattern - The String url pattern.
200
+ # context - The Object context
201
+ # Return url pathname
202
+ def self.sub_url(url_pattern,context)
203
+ url_pattern.gsub(/\{([^{}\/]+)\}/) do
204
+ m=$1
205
+ key_path=m.strip.split '.'
206
+ key_path.reduce(context) do |o,key|
207
+ v = if o.respond_to? key
208
+ o.send key
209
+ elsif (o.respond_to? :[])
210
+ o[key] || o[key.to_sym]
211
+ end
212
+ next v if v
213
+ raise BadUrlPatternException,"Unresolved property `#{m}` in url pattern [#{url_pattern}]!"
214
+ end
215
+ end
216
+ end
217
+
218
+ def _detectContainsPosts(item,posts)
219
+ return posts.include? item.path if Post===item
220
+ return posts.any? {|p|item.include? p} if PostList===item || PostListPage===item
221
+ if Hash===item
222
+ item.each do |k,v|
223
+ return true if _detectContainsPosts(v,posts)
224
+ end
225
+ elsif Array===item
226
+ item.each do |a|
227
+ return true if _detectContainsPosts(a,posts)
228
+ end
229
+ end
230
+ return false
231
+ end
232
+
233
+ end
234
+
235
+ end