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,330 @@
1
+ #coding:utf-8
2
+ module PlainSite
3
+ require 'git'
4
+ require 'uri'
5
+ require 'pathname'
6
+ require 'fileutils'
7
+ require 'webrick'
8
+ require 'listen'
9
+ require 'PlainSite/Data/Category'
10
+ require 'PlainSite/Data/Post'
11
+ require 'PlainSite/RenderTask'
12
+ require 'PlainSite/Utils'
13
+ require 'PlainSite/SocketPatch'
14
+
15
+ SELF_SRC_DIR=File.realpath File.dirname(__FILE__)
16
+ SCAFFOLD_DIR=File.join(SELF_SRC_DIR,'_scaffold')
17
+
18
+ class Site
19
+ attr_reader(
20
+ :root,
21
+ :local,
22
+ :assets_path,
23
+ :dest, # Alter build destination directory,default same as root
24
+ :data_path, # The String data posts directory path
25
+ :templates_path # The String templates directory path
26
+ )
27
+
28
+ attr_accessor :_cur_page_dir # global var -_-!
29
+
30
+ # Params
31
+ # root - The String root path of site,must be an exists path
32
+ def initialize(root)
33
+ @root= File.realpath root
34
+ @dest= @root
35
+ @src_path=File.join(@root,'_src')
36
+ @data_path=File.join(@src_path,'data')
37
+ @routes_rb=File.join(@src_path,'routes.rb')
38
+ @templates_path=File.join(@src_path,'templates')
39
+ @assets_path=File.join(@src_path,'assets')
40
+ @config_file= File.join(@src_path,'config.yml')
41
+ @extensions= File.join(@src_path,'extensions')
42
+
43
+ @_cur_page_dir = ''
44
+
45
+ load_extensions
46
+ create_pygments_css
47
+ end
48
+
49
+ # Reload,clean cached instance variables read from file
50
+ def reload
51
+ @config=nil
52
+ @render_task=nil
53
+ @data=nil
54
+ create_pygments_css
55
+ load_extensions
56
+ end
57
+
58
+ # The Hash config defined if config.yml
59
+ def config
60
+ return @config if @config
61
+ @config = (File.exists? @config_file) ? (YAML.safe_load_file @config_file) : {}
62
+ end
63
+
64
+ # Access config.yml data through site's property
65
+ def method_missing(name,*args,&block)
66
+ return config[name.to_s] if args.empty? && block.nil? && (config.key? name.to_s)
67
+ super
68
+ end
69
+
70
+ # Return the Category object represents _site/data
71
+ def data
72
+ return @data if @data
73
+ @data=Data::Category.new @data_path,self
74
+ end
75
+
76
+ # Init site structure
77
+ def init_scaffold(override=false)
78
+ Utils.merge_folder SCAFFOLD_DIR,@root,override
79
+ reload
80
+ end
81
+
82
+ # Create a new post file
83
+ def newpost(p,title)
84
+ ext=File.extname p
85
+ unless ext.empty? || (Data::Post.extname_ok? p)
86
+ raise Exception,"Unsupported file type:#{ext}.Supported extnames:"+Data::Post.extnames.join(",")
87
+ end
88
+
89
+ name=File.basename p,ext
90
+ ext='.'+Data::Post.extnames[0] unless ext
91
+ name= "#{name}#{ext}"
92
+
93
+ if Data::Post::DATE_NAME_RE =~ name
94
+ date=''
95
+ else
96
+ date="date: #{Date.today}\n"
97
+ end
98
+
99
+ if p['/']
100
+ path=File.join @data_path,(File.dirname p)
101
+ FileUtils.mkdir_p path
102
+ else
103
+ path=@data_path
104
+ end
105
+ path="#{path}/#{name}"
106
+ File.open(path,'wb') do |f|
107
+ f.write "---\ntitle: #{title}\n#{date}---\n\n#{title}\n====="
108
+ end
109
+ path
110
+ end
111
+
112
+ # Config route specified data with ordered template and url path.
113
+ # See: RenderTask#route
114
+ def route(opt)
115
+ render_task.route(opt)
116
+ end
117
+
118
+ # Get the url for object
119
+ # obj - The Post|PageListPage|Category|String
120
+ # Return the String url prefix with site root url(site.url) or relative path if build --local
121
+ def url_for(x)
122
+ render_task.url_for(x)
123
+ end
124
+
125
+ # Build static pages
126
+ # all - The Boolean value to force build all posts.Default only build updated posts.
127
+ # dest - The String path of destination directory
128
+ # includes - The String[] path of posts or templates to force regeneration
129
+ def build(opts={})
130
+ @local=opts[:local]
131
+ @dest= opts[:dest] if opts[:dest]
132
+ includes= opts[:includes]
133
+ if includes && ! includes.empty?
134
+ includes.map! &(File.method :realpath)
135
+ else
136
+ includes=nil
137
+ end
138
+
139
+
140
+ files=diff_files includes
141
+
142
+ if opts[:all] || files.nil?
143
+ render_task.render
144
+ else
145
+ render_task.render(files)
146
+ end
147
+ copy_assets
148
+ end
149
+
150
+ # Clean isolated files under dest directory
151
+ # If file is neither generated by `routes.rb` nor copied from `_src/assets/`,it will be deleted.
152
+ def clean
153
+ isolated_files.each do |f|
154
+ File.delete f
155
+ end
156
+ end
157
+
158
+ def isolated_files
159
+ all_path= render_task.all_urlpath.map do |p|
160
+ File.join @dest,p
161
+ end
162
+ files= (Dir.glob "#{@dest}/*")
163
+ files.reject! {|f| f==@src_path} # Skip src
164
+ _isolated_files= files.reduce([]) do |a,f|
165
+ if File.directory? f
166
+ # concat is not pure
167
+ a.concat (Dir.glob "#{f}/**/**")
168
+ else
169
+ a.push f
170
+ end
171
+ next a
172
+ end
173
+ _isolated_files.select! do |f|
174
+ next false if all_path.include? f
175
+ f=File.join @assets_path,f[@dest.length..-1]
176
+ not (File.exists? f) # Keep static assets
177
+ end
178
+
179
+ return _isolated_files
180
+ end
181
+
182
+ # Run a preview server on localhost:1990
183
+ def serve(opts={})
184
+ host=opts[:host] || 'localhost'
185
+ port=opts[:port] || '1990'
186
+ origin_url=config['url']
187
+
188
+ Listen.to(@src_path) do |m, a, d|
189
+ puts "\nReloaded!\n"
190
+ self.reload
191
+ end
192
+
193
+ server = WEBrick::HTTPServer.new(Port:port,BindAddress:'0.0.0.0')
194
+ server.mount_proc '/' do |req,res|
195
+ url= req.path_info
196
+ url= '/index.html' if url=='/'
197
+ res.status=404 if url=='/404.html'
198
+ prevent_caching(res)
199
+ static_file=File.join @assets_path,url
200
+ if (File.exists? static_file) && !(File.directory? static_file)
201
+ serve_static server,static_file,req,res
202
+ next
203
+ end
204
+ config['url']= "http://#{host}:#{port}"
205
+ result=render_task.render_url url
206
+ config['url']=origin_url
207
+ if result
208
+ res.body=result
209
+ res['Content-Type']='text/html'
210
+ if req.request_method == 'HEAD'
211
+ res['Content-Length'] = 0
212
+ end
213
+ next
214
+ end
215
+ static_file=File.join @dest,url
216
+ if File.exists? static_file
217
+ serve_static server,static_file,req,res
218
+ next
219
+ end
220
+ res.status=301
221
+ res['Location']='/404.html'
222
+ end
223
+ t = Thread.new { server.start }
224
+
225
+ quitServer = proc { exit;server.shutdown }
226
+ trap('INT',quitServer)
227
+ trap('TERM',quitServer)
228
+
229
+ puts "\nServer running on http://#{host}:#{port}/\n"
230
+ t.join
231
+
232
+ end
233
+
234
+ # Get diff_files
235
+ #
236
+ # Return Hash
237
+ # Structure:
238
+ # {
239
+ # updated_posts:[],
240
+ # updated_templates:[],
241
+ # has_deleted_posts:Bool
242
+ # }
243
+ def diff_files(includes=nil)
244
+ deleted_posts=[]
245
+ begin
246
+ repo=Git.open @root
247
+ files=%w(untracked added changed).map {|m|(repo.status.send m).keys}.flatten
248
+ files.map! {|f|File.join @root,f}
249
+ deleted_posts=repo.status.deleted.keys.map {|f|File.join @root,f}
250
+ deleted_posts.select! do |f|
251
+ f.start_with? @data_path
252
+ end
253
+ rescue Git::GitExecuteError,ArgumentError => e
254
+ $stderr.puts("\nMaybe site root is not a valid git repository:#{@root}. Error: " + e.to_s)
255
+ return nil if includes.nil?
256
+ end
257
+ files||=[]
258
+ files.concat includes if includes
259
+ files=files.group_by do |f|
260
+ if f.start_with? @data_path+'/'
261
+ :updated_posts
262
+ elsif f.start_with? @templates_path+'/'
263
+ :updated_templates
264
+ else
265
+ :unrelated
266
+ end
267
+ end
268
+ files.delete :unrelated
269
+ files[:has_deleted_posts]= ! deleted_posts.empty?
270
+ files
271
+ end
272
+
273
+
274
+ private
275
+ def render_task
276
+ return @render_task if @render_task
277
+ @render_task=RenderTask.new self
278
+
279
+ $site=self
280
+ load @routes_rb,true
281
+ @render_task
282
+ end
283
+
284
+ def serve_static(server,static_file,req,res)
285
+ handler=WEBrick::HTTPServlet::DefaultFileHandler.new server,static_file
286
+ handler.do_GET req,res
287
+ end
288
+
289
+ def prevent_caching(res)
290
+ res['ETag'] = nil
291
+ res['Last-Modified'] = Time.now + 100**4
292
+ res['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
293
+ res['Pragma'] = 'no-cache'
294
+ res['Expires'] = Time.now - 100**4
295
+ end
296
+
297
+
298
+ # Copy _src/assets to dest root directory
299
+ def copy_assets
300
+ Utils.merge_folder @assets_path,@dest,true
301
+ end
302
+
303
+ def load_extensions
304
+ if File.directory? @extensions
305
+ (Dir.glob "#{@extensions}/*.rb").each do |f|
306
+ load f
307
+ end
308
+ end
309
+ end
310
+
311
+ def create_pygments_css
312
+ return unless config['code_highlight'] && config['code_highlight']['engine']=='pygments'
313
+ cls='.highlight'
314
+ css_list=config['code_highlight']['pygments_css_list']
315
+ css_list= css_list.each_pair.to_a if css_list
316
+ css_list=[:native,'/css/pygments.css'] unless css_list
317
+ css_list.each do |a|
318
+ style,css_path=a
319
+ css_path=File.join @assets_path,css_path
320
+ next if File.exists? css_path
321
+ FileUtils.mkdir_p File.dirname(css_path)
322
+ css_content=Pygments.css(cls,style:style)
323
+ File.open(css_path,'wb') do |f|
324
+ f.write css_content
325
+ end
326
+ end
327
+ end
328
+
329
+ end
330
+ end
@@ -0,0 +1,15 @@
1
+
2
+ #coding:utf-8
3
+ module PlainSite
4
+ require 'socket'
5
+ module SocketPatch
6
+ class ::TCPSocket
7
+ def peeraddr(*args,&block)
8
+ # Prevent reverse hostname resolve
9
+ args.push :numeric unless args.include? :numeric
10
+ super *args,&block
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,55 @@
1
+ #coding:utf-8
2
+
3
+ module PlainSite;end
4
+ module PlainSite::Tpl
5
+ require 'erb'
6
+ module ExtMethods
7
+ require 'uri'
8
+ require 'json'
9
+ require 'securerandom'
10
+ include ERB::Util
11
+ def echo_block(&block)
12
+ old=@_erbout_buf
13
+ @_erbout_buf=""
14
+ block.call
15
+ block_content=@_erbout_buf.strip
16
+ @_erbout_buf=old
17
+ block_content
18
+ end
19
+
20
+ def raw(&block)
21
+ code=echo_block &block
22
+ @_erbout_buf << (html_escape code)
23
+ end
24
+
25
+ def iframe(attrs={},&block)
26
+ attrs[:width]=attrs[:width] || "100%"
27
+ attrs[:height]=attrs[:height] || "100%"
28
+ attrs= attrs.to_a.map do |a|
29
+ k,v=a
30
+ "#{k}=\"#{v}\""
31
+ end.join " "
32
+ html=echo_block &block
33
+ html="
34
+ <!DOCTYPE html>
35
+ <html>
36
+ <head>
37
+ <title>IFrame</title>
38
+ </head>
39
+ <body>#{html}</body>
40
+ </html>
41
+ "
42
+
43
+ html=html.to_json
44
+ id='ID_'+(SecureRandom.uuid.gsub '-','')
45
+ @_erbout_buf << "
46
+ <iframe #{attrs} src='about:blank' id='#{id}'></iframe>
47
+ <script>
48
+ setTimeout(function () {
49
+ document.getElementById('#{id}').contentWindow.document.write(#{html})
50
+ },0);
51
+ </script>
52
+ "
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,73 @@
1
+ #coding:utf-8
2
+ module PlainSite;end
3
+ module PlainSite::Tpl
4
+ require 'erb'
5
+ require 'PlainSite/Data/FrontMatterFile'
6
+ require 'PlainSite/Utils'
7
+ require 'PlainSite/Tpl/ExtMethods'
8
+
9
+ class LayoutNameException<Exception;end
10
+
11
+ # Layout enhanced ERB.Template file is also YAMLFrontMatterFile
12
+ # Example Template Files:
13
+ # body.html :
14
+ # ---
15
+ # layout: layout.html
16
+ # ---
17
+ # Store layout content :<% content_for :name %>CONTENT<%end%>
18
+ #
19
+ # layout.html :
20
+ # Retrieve content: <%=yield :name%>
21
+ class LayErb
22
+ # Huh? For short name!
23
+ ObjectProxy=PlainSite::Utils::ObjectProxy # module include has many pitfalls
24
+ def initialize(path)
25
+ @path=path
26
+ @template_file=PlainSite::Data::FrontMatterFile.new path
27
+ @layout=@template_file.headers['layout']
28
+ end
29
+ # Render template with context data
30
+ # context - The Object|Hash data
31
+ # yield_contents - The Hash for layout yield retrieves
32
+ def render(context,yield_contents={})
33
+ context=ObjectProxy.new context unless ObjectProxy===context
34
+ contents_store={}
35
+ context.define_singleton_method(:content_for) do |name,&block|
36
+ contents_store[name.to_sym]=echo_block &block
37
+ nil
38
+ end unless context.respond_to? :content_for
39
+
40
+ tpl_path=@path
41
+ context.define_singleton_method(:include) do |file|
42
+ file=File.join File.dirname(tpl_path),file
43
+ new_context=context.dup
44
+ LayErb.new(file).render new_context
45
+ end unless context.respond_to? :include
46
+
47
+ begin
48
+ result=LayErb.render_s(@template_file.content,context,yield_contents)
49
+ rescue Exception=>e
50
+ $stderr.puts "\nError in template:#{@path}\n"
51
+ raise e
52
+ end
53
+ if @layout
54
+ layout_path=File.join (File.dirname @path), @layout
55
+ return LayErb.new(layout_path).render context,contents_store
56
+ end
57
+ result
58
+ end
59
+
60
+ # Render content with context data
61
+ # content - The String template content
62
+ # context - The Object|Hash data
63
+ # yield_contents - The Hash for layout yield retrieve
64
+ def self.render_s(content,context,yield_contents={})
65
+ context=ObjectProxy.new context unless ObjectProxy===context
66
+ context.singleton_class.class_eval { include ExtMethods }
67
+ erb=ERB.new content,nil,nil,'@_erbout_buf'
68
+ result=erb.result(context.get_binding { |k| yield_contents[k.to_sym] })
69
+ result.strip
70
+ end
71
+ end
72
+
73
+ end