PlainSite 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +43 -0
- data/LICENSE +20 -0
- data/PlainSite.gemspec +40 -0
- data/README.md +276 -0
- data/Rakefile +34 -0
- data/_config.yml +6 -0
- data/bin/plainsite +76 -0
- data/lib/PlainSite.rb +5 -0
- data/lib/PlainSite/Commands.rb +76 -0
- data/lib/PlainSite/Data/Category.rb +235 -0
- data/lib/PlainSite/Data/FrontMatterFile.rb +64 -0
- data/lib/PlainSite/Data/Post.rb +237 -0
- data/lib/PlainSite/Data/PostList.rb +164 -0
- data/lib/PlainSite/Data/PostListPage.rb +80 -0
- data/lib/PlainSite/RenderTask.rb +235 -0
- data/lib/PlainSite/Site.rb +330 -0
- data/lib/PlainSite/SocketPatch.rb +15 -0
- data/lib/PlainSite/Tpl/ExtMethods.rb +55 -0
- data/lib/PlainSite/Tpl/LayErb.rb +73 -0
- data/lib/PlainSite/Utils.rb +79 -0
- data/lib/PlainSite/_scaffold/_src/assets/README.md +5 -0
- data/lib/PlainSite/_scaffold/_src/assets/css/style.css +506 -0
- data/lib/PlainSite/_scaffold/_src/assets/favicon.ico +0 -0
- data/lib/PlainSite/_scaffold/_src/config.yml +10 -0
- data/lib/PlainSite/_scaffold/_src/data/essays/game-of-life.md +15 -0
- data/lib/PlainSite/_scaffold/_src/data/essays/phoenix-rebirth.html +15 -0
- data/lib/PlainSite/_scaffold/_src/data/programming/hello-world.md +48 -0
- data/lib/PlainSite/_scaffold/_src/extensions/TplExt.rb +23 -0
- data/lib/PlainSite/_scaffold/_src/routes.rb +49 -0
- data/lib/PlainSite/_scaffold/_src/templates/404.html +16 -0
- data/lib/PlainSite/_scaffold/_src/templates/about.html +11 -0
- data/lib/PlainSite/_scaffold/_src/templates/base.html +32 -0
- data/lib/PlainSite/_scaffold/_src/templates/header.html +8 -0
- data/lib/PlainSite/_scaffold/_src/templates/index.html +25 -0
- data/lib/PlainSite/_scaffold/_src/templates/list.html +41 -0
- data/lib/PlainSite/_scaffold/_src/templates/post.html +81 -0
- data/lib/PlainSite/_scaffold/_src/templates/rss.erb +29 -0
- data/test/CategoryTest.rb +63 -0
- data/test/FrontMatterFileTest.rb +40 -0
- data/test/LayErbTest.rb +20 -0
- data/test/ObjectProxyTest.rb +30 -0
- data/test/PostListTest.rb +55 -0
- data/test/PostTest.rb +48 -0
- data/test/SiteTest.rb +105 -0
- data/test/fixtures/2012-06-12-test.md +7 -0
- data/test/fixtures/category-demo/2012-06-12-post1.md +7 -0
- data/test/fixtures/category-demo/2013-05-01-post2.md +7 -0
- data/test/fixtures/category-demo/_meta.yml +1 -0
- data/test/fixtures/category-demo/index.md +6 -0
- data/test/fixtures/category-demo/sub-category1/sub-post1.md +1 -0
- data/test/fixtures/category-demo/sub-category2/sub-post2.md +1 -0
- data/test/fixtures/include.erb +1 -0
- data/test/fixtures/invalid-front-matter.html +7 -0
- data/test/fixtures/layout.erb +1 -0
- data/test/fixtures/no-front-matter.html +2 -0
- data/test/fixtures/tpl.erb +7 -0
- data/test/fixtures/valid-front-matter.html +14 -0
- data/test/runtest +6 -0
- 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
|