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.
- 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
|