khaleesi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +7 -0
  3. data/LICENSE +22 -0
  4. data/bin/khaleesi +10 -0
  5. data/khaleesi.gemspec +18 -0
  6. data/lib/khaleesi.rb +12 -0
  7. data/lib/khaleesi/about.rb +23 -0
  8. data/lib/khaleesi/cli.rb +641 -0
  9. data/lib/khaleesi/generator.rb +635 -0
  10. data/test/khaleesi_commands_test.rb +50 -0
  11. data/test/khaleesi_test.rb +171 -0
  12. data/test/resources/java-samplecode +9 -0
  13. data/test/resources/test_parse_single_file.md +26 -0
  14. data/test/resources/test_parse_single_file_enable_linenumbers_result +25 -0
  15. data/test/resources/test_parse_single_file_without_linenumbers_result +20 -0
  16. data/test/resources/test_site/_decorators/basic.html +25 -0
  17. data/test/resources/test_site/_decorators/code_snippet.html +8 -0
  18. data/test/resources/test_site/_decorators/post.html +6 -0
  19. data/test/resources/test_site/_decorators/theme.html +15 -0
  20. data/test/resources/test_site/_pages/index.html +49 -0
  21. data/test/resources/test_site/_pages/javasmpcode.md +11 -0
  22. data/test/resources/test_site/_pages/posts/2013/netroid-introduction.md +11 -0
  23. data/test/resources/test_site/_pages/posts/2014/khaleesi-introduction.md +8 -0
  24. data/test/resources/test_site/_pages/studio/cameras/camera1.md +6 -0
  25. data/test/resources/test_site/_pages/studio/cameras/camera2.md +6 -0
  26. data/test/resources/test_site/_pages/studio/cameras/camera3.md +6 -0
  27. data/test/resources/test_site/_pages/studio/cameras/camera4.md +6 -0
  28. data/test/resources/test_site/_pages/themes/base16_solarized.md +119 -0
  29. data/test/resources/test_site/_pages/themes/base16_solarized_dark.md +117 -0
  30. data/test/resources/test_site/_pages/themes/github.md +272 -0
  31. data/test/resources/test_site/_pages/themes_illustrates/base16_solarized.html +4 -0
  32. data/test/resources/test_site/_pages/themes_illustrates/base16_solarized_dark.html +4 -0
  33. data/test/resources/test_site/_pages/themes_illustrates/github.html +4 -0
  34. data/test/resources/test_site/_pages/themes_illustrates/monokai.html +4 -0
  35. data/test/resources/test_site/_pages/themes_illustrates/monokai_sublime.html +4 -0
  36. data/test/resources/test_site/_raw/css/site.css +340 -0
  37. data/test/resources/test_site/expected_base16_solarized_dark_html +146 -0
  38. data/test/resources/test_site/expected_base16_solarized_html +150 -0
  39. data/test/resources/test_site/expected_github_html +301 -0
  40. data/test/resources/test_site/expected_index_html +373 -0
  41. 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