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