khaleesi 0.1.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 +7 -0
- data/LICENSE +22 -0
- data/bin/khaleesi +10 -0
- data/khaleesi.gemspec +18 -0
- data/lib/khaleesi.rb +12 -0
- data/lib/khaleesi/about.rb +23 -0
- data/lib/khaleesi/cli.rb +641 -0
- data/lib/khaleesi/generator.rb +635 -0
- data/test/khaleesi_commands_test.rb +50 -0
- data/test/khaleesi_test.rb +171 -0
- data/test/resources/java-samplecode +9 -0
- data/test/resources/test_parse_single_file.md +26 -0
- data/test/resources/test_parse_single_file_enable_linenumbers_result +25 -0
- data/test/resources/test_parse_single_file_without_linenumbers_result +20 -0
- data/test/resources/test_site/_decorators/basic.html +25 -0
- data/test/resources/test_site/_decorators/code_snippet.html +8 -0
- data/test/resources/test_site/_decorators/post.html +6 -0
- data/test/resources/test_site/_decorators/theme.html +15 -0
- data/test/resources/test_site/_pages/index.html +49 -0
- data/test/resources/test_site/_pages/javasmpcode.md +11 -0
- data/test/resources/test_site/_pages/posts/2013/netroid-introduction.md +11 -0
- data/test/resources/test_site/_pages/posts/2014/khaleesi-introduction.md +8 -0
- data/test/resources/test_site/_pages/studio/cameras/camera1.md +6 -0
- data/test/resources/test_site/_pages/studio/cameras/camera2.md +6 -0
- data/test/resources/test_site/_pages/studio/cameras/camera3.md +6 -0
- data/test/resources/test_site/_pages/studio/cameras/camera4.md +6 -0
- data/test/resources/test_site/_pages/themes/base16_solarized.md +119 -0
- data/test/resources/test_site/_pages/themes/base16_solarized_dark.md +117 -0
- data/test/resources/test_site/_pages/themes/github.md +272 -0
- data/test/resources/test_site/_pages/themes_illustrates/base16_solarized.html +4 -0
- data/test/resources/test_site/_pages/themes_illustrates/base16_solarized_dark.html +4 -0
- data/test/resources/test_site/_pages/themes_illustrates/github.html +4 -0
- data/test/resources/test_site/_pages/themes_illustrates/monokai.html +4 -0
- data/test/resources/test_site/_pages/themes_illustrates/monokai_sublime.html +4 -0
- data/test/resources/test_site/_raw/css/site.css +340 -0
- data/test/resources/test_site/expected_base16_solarized_dark_html +146 -0
- data/test/resources/test_site/expected_base16_solarized_html +150 -0
- data/test/resources/test_site/expected_github_html +301 -0
- data/test/resources/test_site/expected_index_html +373 -0
- metadata +86 -0
@@ -0,0 +1,635 @@
|
|
1
|
+
module Khaleesi
|
2
|
+
class Generator
|
3
|
+
|
4
|
+
# The constructor accepts all settings then keep them as fields, lively in whole processing job.
|
5
|
+
def initialize(opts={})
|
6
|
+
# source directory path (must absolutely).
|
7
|
+
@src_dir = opts[:src_dir].to_s
|
8
|
+
|
9
|
+
# destination directory path (must absolutely).
|
10
|
+
@dest_dir = opts[:dest_dir].to_s
|
11
|
+
|
12
|
+
# setting to tell syntax highlighter output line numbers.
|
13
|
+
$line_numbers = opts[:line_numbers].to_s.eql?('true')
|
14
|
+
|
15
|
+
# a css class name which developer wants to customizable.
|
16
|
+
$css_class = opts[:css_class] || 'highlight'
|
17
|
+
|
18
|
+
# a full time pattern used to including date and time like '2014-08-22 16:45'.
|
19
|
+
# see http://www.ruby-doc.org/core-2.1.2/Time.html#strftime-method for pattern details.
|
20
|
+
@time_pattern = opts[:time_pattern] || '%a %e %b %H:%M %Y'
|
21
|
+
|
22
|
+
# a short time pattern used to display only date like '2014-08-22'.
|
23
|
+
@date_pattern = opts[:date_pattern] || '%F'
|
24
|
+
|
25
|
+
# we just pick on those pages who changed but haven't commit
|
26
|
+
# to git repository to generate, ignore the unchanged pages.
|
27
|
+
# this action could be a huge benefit when you were creating
|
28
|
+
# a new page and you want just to focusing that page at all.
|
29
|
+
@diff_plus = opts[:diff_plus].to_s.eql?('true')
|
30
|
+
|
31
|
+
# indicating which syntax highlighter would be used, default is Rouge.
|
32
|
+
$use_pygments = opts[:highlighter].to_s.eql?('pygments')
|
33
|
+
|
34
|
+
# specify which headers will generate a "Table of Contents" id, leave empty means disable TOC generation.
|
35
|
+
$toc_selection = opts[:toc_selection].to_s
|
36
|
+
|
37
|
+
@decrt_regexp = produce_variable_regex('decorator')
|
38
|
+
@title_regexp = produce_variable_regex('title')
|
39
|
+
@var_regexp = /(\p{Word}+):(\p{Word}+)/
|
40
|
+
@doc_regexp = /^‡{6,}$/
|
41
|
+
|
42
|
+
@page_dir = "#{@src_dir}/_pages/"
|
43
|
+
|
44
|
+
# a cascaded variable stack that storing a set of page's variable while generating,
|
45
|
+
# able for each handling page to grab it parent's variable and parent's variable.
|
46
|
+
@variable_stack = Array.new
|
47
|
+
|
48
|
+
# a queue that storing valid pages, use to avoid invalid page(decorator file)
|
49
|
+
# influence the page link, page times generation.
|
50
|
+
@page_stack = Array.new
|
51
|
+
end
|
52
|
+
|
53
|
+
# Main entry of Generator that generates all the pages of the site,
|
54
|
+
# it scan the source directory files that obey the rule of page,
|
55
|
+
# evaluates and applies all predefine logical, writes the final
|
56
|
+
# content into destination directory cascaded.
|
57
|
+
def generate
|
58
|
+
start_time = Time.now
|
59
|
+
|
60
|
+
Dir.glob("#{@page_dir}/**/*") do |page_file|
|
61
|
+
next unless File.readable? page_file
|
62
|
+
next unless is_valid_file page_file
|
63
|
+
|
64
|
+
$toc_index = 0
|
65
|
+
@page_stack.clear
|
66
|
+
@page_stack.push File.expand_path(page_file)
|
67
|
+
single_start_time = Time.now
|
68
|
+
|
69
|
+
if @diff_plus
|
70
|
+
file_status = nil
|
71
|
+
base_name = File.basename(page_file)
|
72
|
+
Dir.chdir(File.expand_path('..', page_file)) do
|
73
|
+
file_status = %x[git status -s #{base_name} 2>&1]
|
74
|
+
end
|
75
|
+
file_status = file_status.to_s.strip
|
76
|
+
|
77
|
+
# only haven't commit pages available, Git will return nothing if page committed.
|
78
|
+
next if file_status.empty?
|
79
|
+
|
80
|
+
# a correct message from Git should included the file name, may occur errors
|
81
|
+
# in command running such as Git didn't install if not include.
|
82
|
+
unless file_status.include? base_name
|
83
|
+
puts file_status
|
84
|
+
next
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
extract_page_structure(page_file)
|
89
|
+
|
90
|
+
variables = @variable_stack.pop
|
91
|
+
# page can't stand without decorator
|
92
|
+
next unless variables and variables[@decrt_regexp, 3]
|
93
|
+
|
94
|
+
# isn't legal page if title missing
|
95
|
+
next unless variables[@title_regexp, 3]
|
96
|
+
|
97
|
+
content = is_html_file(page_file) ? parse_html_file(page_file) : parse_markdown_file(page_file)
|
98
|
+
|
99
|
+
page_path = File.expand_path(@dest_dir + gen_link(page_file, variables))
|
100
|
+
page_dir_path = File.dirname(page_path)
|
101
|
+
unless File.directory?(page_dir_path)
|
102
|
+
FileUtils.mkdir_p(page_dir_path)
|
103
|
+
end
|
104
|
+
|
105
|
+
bytes = IO.write(page_path, content)
|
106
|
+
puts "Done (#{Generator.humanize(Time.now - single_start_time)}) => '#{page_path}' bytes[#{bytes}]."
|
107
|
+
end
|
108
|
+
|
109
|
+
puts "Generator time elapsed : #{Generator.humanize(Time.now - start_time)}."
|
110
|
+
end
|
111
|
+
|
112
|
+
def parse_markdown_file(file_path)
|
113
|
+
content = extract_page_structure(file_path)
|
114
|
+
content = parse_decorator_file(handle_markdown(content))
|
115
|
+
@variable_stack.pop
|
116
|
+
content
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse_decorator_file(bore_content)
|
120
|
+
variables = @variable_stack.last
|
121
|
+
decorator = variables ? variables[@decrt_regexp, 3] : nil
|
122
|
+
decorator ? parse_html_file("#{@src_dir}/_decorators/#{decorator.strip}.html", bore_content) : bore_content
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_html_file(file_path, bore_content=nil)
|
126
|
+
content = extract_page_structure(file_path)
|
127
|
+
|
128
|
+
content = parse_html_content(content.to_s, bore_content)
|
129
|
+
content = parse_decorator_file(content) # recurse parse
|
130
|
+
|
131
|
+
@variable_stack.pop
|
132
|
+
content
|
133
|
+
end
|
134
|
+
|
135
|
+
def parse_html_content(html_content, bore_content)
|
136
|
+
# http://www.ruby-doc.org/core-2.1.0/Regexp.html#class-Regexp-label-Repetition use '.+?' to disable greedy match.
|
137
|
+
regexp = /(#foreach\p{Blank}?\(\$(\p{Graph}+)\p{Blank}?:\p{Blank}?\$(\p{Graph}+)\p{Blank}?(asc|desc)?\p{Blank}?(\d*)\)(.+?)#end)/m
|
138
|
+
while (foreach_snippet = html_content.match(regexp))
|
139
|
+
foreach_snippet = handle_foreach_snippet(foreach_snippet)
|
140
|
+
|
141
|
+
# because the Regexp cannot skip a unhandled foreach snippet, so we claim every
|
142
|
+
# snippet must done successfully, and if not, we shall use blank instead.
|
143
|
+
html_content.sub!(regexp, foreach_snippet.to_s)
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
regexp = /(#if\p{Blank}chain:(prev|next)\(\$(\p{Graph}+)\)(.+?)#end)/m
|
148
|
+
while (chain_snippet = html_content.match(regexp))
|
149
|
+
chain_snippet = handle_chain_snippet(chain_snippet)
|
150
|
+
html_content.sub!(regexp, chain_snippet.to_s)
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# handle the html content after foreach and chain logical, to avoid that
|
155
|
+
# logical included after this handle, such as including markdown files.
|
156
|
+
html_content = handle_html_content(html_content)
|
157
|
+
|
158
|
+
|
159
|
+
# we deal with decorator's content at final because it may slow down
|
160
|
+
# the process even cause errors for the "foreach" and "chain" scripts.
|
161
|
+
html_content.sub!(/\$\{decorator:content}/, bore_content) if bore_content
|
162
|
+
|
163
|
+
html_content
|
164
|
+
end
|
165
|
+
|
166
|
+
def handle_html_content(html_content, added_scope=nil)
|
167
|
+
page_file = @page_stack.last
|
168
|
+
parsed_text = ''
|
169
|
+
sub_script = ''
|
170
|
+
|
171
|
+
# char by char to evaluate html content.
|
172
|
+
html_content.each_char do |char|
|
173
|
+
is_valid = sub_script.start_with?('${')
|
174
|
+
case char
|
175
|
+
when '$'
|
176
|
+
# if met the variable expression beginner, we'll append precede characters to parsed_text
|
177
|
+
# so the invalid part of expression still output as usual text rather than erase them.
|
178
|
+
parsed_text << sub_script unless sub_script.empty?
|
179
|
+
sub_script.clear << char
|
180
|
+
|
181
|
+
when '{', ':'
|
182
|
+
is_valid = sub_script.eql? '$' if char == '{'
|
183
|
+
is_valid = is_valid && sub_script.length > 3 if char == ':'
|
184
|
+
if is_valid
|
185
|
+
sub_script << char
|
186
|
+
else
|
187
|
+
parsed_text << sub_script << char
|
188
|
+
sub_script.clear
|
189
|
+
end
|
190
|
+
|
191
|
+
when '}'
|
192
|
+
is_valid = is_valid && sub_script.length > 4
|
193
|
+
sub_script << char
|
194
|
+
if is_valid
|
195
|
+
|
196
|
+
# parsing variable expressions such as :
|
197
|
+
# ${variable:title}, ${variable:description}, ${custom_scope:custom_value} etc.
|
198
|
+
form_scope = sub_script[@var_regexp, 1]
|
199
|
+
form_value = sub_script[@var_regexp, 2]
|
200
|
+
|
201
|
+
case form_scope
|
202
|
+
when 'variable', added_scope
|
203
|
+
|
204
|
+
case form_value
|
205
|
+
when 'createtime'
|
206
|
+
create_time = Generator.fetch_create_time(page_file)
|
207
|
+
parsed_text << (create_time ? create_time.strftime(@time_pattern) : sub_script)
|
208
|
+
|
209
|
+
when 'createdate'
|
210
|
+
create_time = Generator.fetch_create_time(page_file)
|
211
|
+
parsed_text << (create_time ? create_time.strftime(@date_pattern) : sub_script)
|
212
|
+
|
213
|
+
when 'modifytime'
|
214
|
+
modify_time = Generator.fetch_modify_time(page_file)
|
215
|
+
parsed_text << (modify_time ? modify_time.strftime(@time_pattern) : sub_script)
|
216
|
+
|
217
|
+
when 'modifydate'
|
218
|
+
modify_time = Generator.fetch_modify_time(page_file)
|
219
|
+
parsed_text << (modify_time ? modify_time.strftime(@date_pattern) : sub_script)
|
220
|
+
|
221
|
+
when 'link'
|
222
|
+
page_link = gen_link(page_file, @variable_stack.last)
|
223
|
+
parsed_text << (page_link ? page_link : sub_script)
|
224
|
+
|
225
|
+
else
|
226
|
+
text = nil
|
227
|
+
if form_value.eql?('content') and form_scope.eql?(added_scope)
|
228
|
+
text = parse_html_file(page_file) if is_html_file(page_file)
|
229
|
+
text = parse_markdown_file(page_file) if is_markdown_file(page_file)
|
230
|
+
|
231
|
+
else
|
232
|
+
regexp = /^#{form_value}(\p{Blank}?):(.+)$/
|
233
|
+
@variable_stack.reverse_each do |var|
|
234
|
+
text = var[regexp, 2] if var
|
235
|
+
break if text
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
parsed_text << (text ? text.strip : sub_script)
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
when 'page'
|
245
|
+
match_page = nil
|
246
|
+
Dir.glob("#{@page_dir}/**/#{form_value}.*") do |inner_page|
|
247
|
+
match_page = inner_page
|
248
|
+
break
|
249
|
+
end
|
250
|
+
|
251
|
+
if is_html_file(match_page)
|
252
|
+
@page_stack.push match_page
|
253
|
+
inc_content = parse_html_file(match_page)
|
254
|
+
@page_stack.pop
|
255
|
+
end
|
256
|
+
inc_content = parse_markdown_file(match_page) if is_markdown_file(match_page)
|
257
|
+
|
258
|
+
parsed_text << (inc_content ? inc_content : sub_script)
|
259
|
+
|
260
|
+
else
|
261
|
+
parsed_text << sub_script
|
262
|
+
end
|
263
|
+
|
264
|
+
else
|
265
|
+
parsed_text << sub_script
|
266
|
+
end
|
267
|
+
|
268
|
+
sub_script.clear
|
269
|
+
|
270
|
+
else
|
271
|
+
is_valid = is_valid && char.index(/\p{Graph}/)
|
272
|
+
if is_valid
|
273
|
+
sub_script << char
|
274
|
+
else
|
275
|
+
parsed_text << sub_script << char
|
276
|
+
sub_script.clear
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
parsed_text
|
282
|
+
end
|
283
|
+
|
284
|
+
# Foreach loop was design for traversal all files of directory which inside the "_pages" directory,
|
285
|
+
# each time through the loop, the segment who planning to repeat would be evaluate and output as parsed text.
|
286
|
+
# at the beginning, we'll gather all files and sort by sequence or create time finally produce an ordered list.
|
287
|
+
# NOTE: sub-directory writing was acceptable, also apply order-by-limit mode like SQL to manipulate that list.
|
288
|
+
#
|
289
|
+
# examples :
|
290
|
+
#
|
291
|
+
# loop the whole list :
|
292
|
+
# <ul>
|
293
|
+
# #foreach ($theme : $themes)
|
294
|
+
# <li>${theme:name}</li>
|
295
|
+
# <li>${theme:description}</li>
|
296
|
+
# #end
|
297
|
+
# </ul>
|
298
|
+
#
|
299
|
+
# loop the whole list but sortby descending and limit 5 items.
|
300
|
+
# <ul>
|
301
|
+
# #foreach ($theme : $themes desc 5)
|
302
|
+
# <li>${theme:name}</li>
|
303
|
+
# <li>${theme:description}</li>
|
304
|
+
# #end
|
305
|
+
# </ul>
|
306
|
+
def handle_foreach_snippet(foreach_snippet)
|
307
|
+
dir_path = foreach_snippet[3].prepend(@page_dir)
|
308
|
+
return unless Dir.exists? dir_path
|
309
|
+
|
310
|
+
loop_body = foreach_snippet[6]
|
311
|
+
var_name = foreach_snippet[2]
|
312
|
+
order_by = foreach_snippet[4]
|
313
|
+
limit = foreach_snippet[5].to_i
|
314
|
+
limit = -1 if limit == 0
|
315
|
+
|
316
|
+
page_ary = take_page_array(dir_path)
|
317
|
+
# if sub-term enable descending order, we'll reversing the page stack.
|
318
|
+
page_ary.reverse! if order_by.eql?('desc')
|
319
|
+
|
320
|
+
parsed_body = ''
|
321
|
+
page_ary.each_with_index do |page, index|
|
322
|
+
# abort loop if has limitation.
|
323
|
+
break if index == limit
|
324
|
+
parsed_body << handle_snippet_page(page, loop_body, var_name)
|
325
|
+
end
|
326
|
+
parsed_body
|
327
|
+
end
|
328
|
+
|
329
|
+
# Chain, just as its name meaning, we take the previous or next page from the ordered list
|
330
|
+
# which same of foreach snippet, of course that list contained current page we just
|
331
|
+
# generating on, so we took the near item for it, just make it like a chain.
|
332
|
+
#
|
333
|
+
# examples :
|
334
|
+
#
|
335
|
+
# #if chain:prev($theme)
|
336
|
+
# <div class="prev">Prev Theme : <a href="${theme:link}">${theme:title}</a></div>
|
337
|
+
# #end
|
338
|
+
#
|
339
|
+
# #if chain:next($theme)
|
340
|
+
# <div class="next">Next Theme : <a href="${theme:link}">${theme:title}</a></div>
|
341
|
+
# #end
|
342
|
+
def handle_chain_snippet(chain_snippet)
|
343
|
+
cmd = chain_snippet[2]
|
344
|
+
var_name = chain_snippet[3]
|
345
|
+
loop_body = chain_snippet[4]
|
346
|
+
|
347
|
+
page_ary = take_page_array(File.expand_path('..', @page_stack.first))
|
348
|
+
page_ary.each_with_index do |page, index|
|
349
|
+
next unless page.to_s.eql? @page_stack.first
|
350
|
+
|
351
|
+
page = cmd.eql?('prev') ? page_ary.prev(index) : page_ary.next(index)
|
352
|
+
return page ? handle_snippet_page(page, loop_body, var_name) : nil
|
353
|
+
end
|
354
|
+
nil
|
355
|
+
end
|
356
|
+
|
357
|
+
def handle_snippet_page(page, loop_body, var_name)
|
358
|
+
# make current page properties occupy atop for two stacks while processing such sub-level files.
|
359
|
+
@variable_stack.push(page.instance_variable_get(:@page_variables))
|
360
|
+
@page_stack.push page.to_s
|
361
|
+
|
362
|
+
parsed_body = handle_html_content(loop_body, var_name)
|
363
|
+
|
364
|
+
# abandon that properties immediately.
|
365
|
+
@variable_stack.pop
|
366
|
+
@page_stack.pop
|
367
|
+
|
368
|
+
parsed_body
|
369
|
+
end
|
370
|
+
|
371
|
+
# Search that directory and it's sub-directories, collecting all valid files,
|
372
|
+
# then sorting by sequence or create time before return.
|
373
|
+
def take_page_array(dir_path)
|
374
|
+
page_ary = Array.new
|
375
|
+
Dir.glob("#{dir_path}/**/*") do |page_file|
|
376
|
+
next unless is_valid_file(page_file)
|
377
|
+
|
378
|
+
extract_page_structure(page_file)
|
379
|
+
page_ary.push Page.new(page_file, @variable_stack.pop)
|
380
|
+
end
|
381
|
+
|
382
|
+
page_ary.sort! do |left, right|
|
383
|
+
right <=> left
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# Split by separators, extract page's variables and content.
|
388
|
+
def extract_page_structure(page_file)
|
389
|
+
begin
|
390
|
+
document = IO.read(page_file)
|
391
|
+
rescue Exception => e
|
392
|
+
puts e.message
|
393
|
+
document = ''
|
394
|
+
end
|
395
|
+
|
396
|
+
index = document.index(@doc_regexp).to_i
|
397
|
+
if index > 0
|
398
|
+
@variable_stack.push(document[0, index])
|
399
|
+
document[index..-1].sub(@doc_regexp, '').strip
|
400
|
+
else
|
401
|
+
# we must hold the variable stack.
|
402
|
+
@variable_stack.push(nil)
|
403
|
+
document
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
def gen_link(page_path, variables)
|
408
|
+
# only generate link for title-present page.
|
409
|
+
title = variables[@title_regexp, 3] if variables
|
410
|
+
return unless title
|
411
|
+
|
412
|
+
relative_path = File.dirname(page_path[/(\p{Graph}+)\/_pages(\p{Graph}+)/, 2])
|
413
|
+
relative_path << '/' unless relative_path.end_with? '/'
|
414
|
+
|
415
|
+
# fetch and use the pre-define page name if legal.
|
416
|
+
page_name = variables[produce_variable_regex('slug'), 3]
|
417
|
+
return File.expand_path(relative_path << page_name) unless page_name.strip.empty? if page_name
|
418
|
+
|
419
|
+
# we shall use the page title to generating a link.
|
420
|
+
Generator.format_as_legal_link(title)
|
421
|
+
|
422
|
+
# may hunting down all title's characters, use file name as safe way.
|
423
|
+
title = File.basename(page_path, '.*') if title.empty?
|
424
|
+
|
425
|
+
File.expand_path(relative_path << title << '.html')
|
426
|
+
end
|
427
|
+
|
428
|
+
def self.format_as_legal_link(text)
|
429
|
+
text.strip!
|
430
|
+
# delete else of characters if not [alpha, number, whitespace, dashes, underscore].
|
431
|
+
text.gsub!(/[^0-9a-z \-_]/i, '')
|
432
|
+
# replace whitespace to dashes.
|
433
|
+
text.gsub!(' ', '-')
|
434
|
+
text.squeeze!('-')
|
435
|
+
text.downcase!
|
436
|
+
end
|
437
|
+
|
438
|
+
def self.fetch_create_time(page_file)
|
439
|
+
# fetch the first Git versioned time as create time.
|
440
|
+
fetch_git_time(page_file, 'tail')
|
441
|
+
end
|
442
|
+
|
443
|
+
def self.fetch_modify_time(page_file)
|
444
|
+
# fetch the last Git versioned time as modify time.
|
445
|
+
fetch_git_time(page_file, 'head')
|
446
|
+
end
|
447
|
+
|
448
|
+
# Enter into the file container and take the Git time,
|
449
|
+
# if something wrong with executing(Git didn't install?),
|
450
|
+
# we'll use current time as replacement.
|
451
|
+
def self.fetch_git_time(page_file, cmd)
|
452
|
+
Dir.chdir(File.expand_path('..', page_file)) do
|
453
|
+
commit_time = %x[git log --date=iso --pretty='%cd' #{File.basename(page_file)} 2>&1 | #{cmd} -1]
|
454
|
+
begin
|
455
|
+
# the rightful time looks like this : "2014-08-18 18:44:41 +0800"
|
456
|
+
Time.parse(commit_time)
|
457
|
+
rescue
|
458
|
+
Time.now
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# intercept the Redcarpet processing, do the syntax highlighter with Rouge or Pygments.
|
464
|
+
class HTML < Redcarpet::Render::HTML
|
465
|
+
def block_code(code, language)
|
466
|
+
language = 'text' if language.to_s.strip.empty?
|
467
|
+
css_class = $css_class + ' ' + language
|
468
|
+
return pygments_colorize(css_class, code, language) if $use_pygments
|
469
|
+
rouge_colorize(css_class, code, language)
|
470
|
+
end
|
471
|
+
|
472
|
+
def pygments_colorize(css_class, code, language)
|
473
|
+
opts = {:cssclass => css_class, :linenos => $line_numbers}
|
474
|
+
begin
|
475
|
+
colored_html = Pygments.highlight(code, :lexer => language, :options => opts)
|
476
|
+
rescue MentosError
|
477
|
+
colored_html = Pygments.highlight(code, :lexer => 'text', :options => opts)
|
478
|
+
end
|
479
|
+
return colored_html unless $line_numbers
|
480
|
+
|
481
|
+
# we'll have the html structure consistent whatever line numbers present or not.
|
482
|
+
colored_html.sub!(" class=\"#{css_class}table\"", '')
|
483
|
+
colored_html.sub!('<div class="linenodiv">', '')
|
484
|
+
colored_html.gsub!('</div>', '')
|
485
|
+
colored_html.sub!(' class="code"', '')
|
486
|
+
root_elements = "<div class=\"#{css_class}\">"
|
487
|
+
colored_html.sub!(root_elements, '')
|
488
|
+
colored_html.prepend(root_elements).concat('</div>')
|
489
|
+
colored_html
|
490
|
+
end
|
491
|
+
|
492
|
+
def rouge_colorize(css_class, code, language)
|
493
|
+
formatter = Rouge::Formatters::HTML.new(:css_class => css_class, :line_numbers => $line_numbers)
|
494
|
+
lexer = Rouge::Lexer.find_fancy(language, code) || Rouge::Lexers::PlainText
|
495
|
+
colored_html = formatter.format(lexer.lex(code))
|
496
|
+
|
497
|
+
if $line_numbers
|
498
|
+
colored_html.sub!('class="gutter gl" style="text-align: right"', 'class="linenos"')
|
499
|
+
colored_html.sub!(' style="border-spacing: 0"', '')
|
500
|
+
colored_html.sub!(' class="lineno"', '')
|
501
|
+
colored_html.sub!(' class="code"', '')
|
502
|
+
colored_html.sub!('<tbody>', '')
|
503
|
+
colored_html.sub!('</tbody>', '')
|
504
|
+
else
|
505
|
+
colored_html.sub!("<code class=\"#{css_class}\">", '')
|
506
|
+
colored_html.sub!('</code>', '')
|
507
|
+
colored_html.prepend("<div class=\"#{css_class}\">").concat('</div>')
|
508
|
+
end
|
509
|
+
|
510
|
+
colored_html
|
511
|
+
end
|
512
|
+
|
513
|
+
# intercept header generation, decide which need to output id by settings.
|
514
|
+
def header(title, level)
|
515
|
+
is_unique = $toc_selection.include?('unique')
|
516
|
+
"\n<h%s%s>%s</h%s>\n" % [
|
517
|
+
level, if $toc_selection.include?(level.to_s)
|
518
|
+
" id=\"#{is_unique ? unique_id : header_anchor(title)}\""
|
519
|
+
else
|
520
|
+
''
|
521
|
+
end, title, level]
|
522
|
+
end
|
523
|
+
|
524
|
+
# This method origin from redcarpet-3.1.2/ext/redcarpet/html.c:268.
|
525
|
+
def header_anchor(text)
|
526
|
+
# We must unescape HTML entities because
|
527
|
+
# Redcarpet escape them before.
|
528
|
+
text = CGI.unescapeHTML(text)
|
529
|
+
|
530
|
+
# delete markup entities.
|
531
|
+
text = text.gsub(/<\/?[^>]*>/, '')
|
532
|
+
|
533
|
+
Generator.format_as_legal_link(text)
|
534
|
+
text.length < 3 ? unique_id : text
|
535
|
+
end
|
536
|
+
|
537
|
+
def unique_id
|
538
|
+
"header-#{$toc_index = $toc_index + 1}"
|
539
|
+
end
|
540
|
+
|
541
|
+
def initialize(opts={})
|
542
|
+
opts.store(:with_toc_data, true)
|
543
|
+
# opts.store(:prettify, true)
|
544
|
+
opts.store(:xhtml, true)
|
545
|
+
super
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
def handle_markdown(text)
|
550
|
+
return '' if text.to_s.empty?
|
551
|
+
markdown = Redcarpet::Markdown.new(HTML, fenced_code_blocks: true, autolink: true, no_intra_emphasis: true, strikethrough: true, tables: true)
|
552
|
+
markdown.render(text)
|
553
|
+
end
|
554
|
+
|
555
|
+
def produce_variable_regex(var_name)
|
556
|
+
/^#{var_name}(\p{Blank}?):(\p{Blank}?)(.+)$/
|
557
|
+
end
|
558
|
+
|
559
|
+
def is_valid_file(file_path)
|
560
|
+
is_markdown_file(file_path) or is_html_file(file_path)
|
561
|
+
end
|
562
|
+
|
563
|
+
def is_markdown_file(file_path)
|
564
|
+
file_path and file_path.end_with? '.md'
|
565
|
+
end
|
566
|
+
|
567
|
+
def is_html_file(file_path)
|
568
|
+
file_path and file_path.end_with? '.html'
|
569
|
+
end
|
570
|
+
|
571
|
+
def self.humanize(secs) # http://stackoverflow.com/a/4136485/1294681
|
572
|
+
secs = secs * 1000
|
573
|
+
[[1000, :milliseconds], [60, :seconds], [60, :minutes]].map { |count, name|
|
574
|
+
if secs > 0
|
575
|
+
secs, n = secs.divmod(count)
|
576
|
+
n.to_i > 0 ? "#{n.to_i} #{name}" : ''
|
577
|
+
end
|
578
|
+
}.compact.reverse.join(' ').squeeze(' ').strip
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
class Page
|
583
|
+
@page_file
|
584
|
+
@page_variables
|
585
|
+
|
586
|
+
def initialize(page_file, page_variables)
|
587
|
+
@page_file = File.expand_path(page_file)
|
588
|
+
@page_variables = page_variables
|
589
|
+
end
|
590
|
+
|
591
|
+
def <=> (other)
|
592
|
+
regexp = /^sequence(\p{Blank}?):(\p{Blank}?)(\d+)$/
|
593
|
+
|
594
|
+
self_sequence = @page_variables[regexp, 3] if @page_variables
|
595
|
+
o_variables = other.instance_variable_get(:@page_variables)
|
596
|
+
other_sequence = o_variables[regexp, 3] if o_variables
|
597
|
+
|
598
|
+
# if which one specify sequence, we shall force comparing by sequence.
|
599
|
+
if self_sequence || other_sequence
|
600
|
+
self_sequence = self_sequence.to_i
|
601
|
+
other_sequence = other_sequence.to_i
|
602
|
+
|
603
|
+
return 1 if self_sequence < other_sequence
|
604
|
+
return 0 if self_sequence == other_sequence
|
605
|
+
return -1 if self_sequence > other_sequence
|
606
|
+
end
|
607
|
+
|
608
|
+
|
609
|
+
other_create_time = other.take_create_time
|
610
|
+
self_create_time = take_create_time
|
611
|
+
other_create_time <=> self_create_time
|
612
|
+
end
|
613
|
+
|
614
|
+
@create_time
|
615
|
+
def take_create_time
|
616
|
+
# cache the create time to improve performance while sorting by.
|
617
|
+
return @create_time if @create_time
|
618
|
+
@create_time = Generator.fetch_create_time(@page_file)
|
619
|
+
end
|
620
|
+
|
621
|
+
def to_s
|
622
|
+
@page_file
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
class Array < Array
|
627
|
+
def next(index)
|
628
|
+
index += 1
|
629
|
+
index < size ? at(index) : nil
|
630
|
+
end
|
631
|
+
def prev(index)
|
632
|
+
index > 0 ? at(index - 1) : nil
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end
|