PlainSite 1.2.0

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