bunto 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.markdown +59 -0
  4. data/bin/bunto +51 -0
  5. data/lib/bunto.rb +179 -0
  6. data/lib/bunto/cleaner.rb +105 -0
  7. data/lib/bunto/collection.rb +205 -0
  8. data/lib/bunto/command.rb +65 -0
  9. data/lib/bunto/commands/build.rb +77 -0
  10. data/lib/bunto/commands/clean.rb +42 -0
  11. data/lib/bunto/commands/doctor.rb +114 -0
  12. data/lib/bunto/commands/help.rb +31 -0
  13. data/lib/bunto/commands/new.rb +82 -0
  14. data/lib/bunto/commands/serve.rb +204 -0
  15. data/lib/bunto/commands/serve/servlet.rb +61 -0
  16. data/lib/bunto/configuration.rb +323 -0
  17. data/lib/bunto/converter.rb +48 -0
  18. data/lib/bunto/converters/identity.rb +21 -0
  19. data/lib/bunto/converters/markdown.rb +92 -0
  20. data/lib/bunto/converters/markdown/kramdown_parser.rb +117 -0
  21. data/lib/bunto/converters/markdown/rdiscount_parser.rb +33 -0
  22. data/lib/bunto/converters/markdown/redcarpet_parser.rb +102 -0
  23. data/lib/bunto/converters/smartypants.rb +34 -0
  24. data/lib/bunto/convertible.rb +297 -0
  25. data/lib/bunto/deprecator.rb +46 -0
  26. data/lib/bunto/document.rb +444 -0
  27. data/lib/bunto/drops/bunto_drop.rb +21 -0
  28. data/lib/bunto/drops/collection_drop.rb +22 -0
  29. data/lib/bunto/drops/document_drop.rb +27 -0
  30. data/lib/bunto/drops/drop.rb +176 -0
  31. data/lib/bunto/drops/site_drop.rb +38 -0
  32. data/lib/bunto/drops/unified_payload_drop.rb +25 -0
  33. data/lib/bunto/drops/url_drop.rb +83 -0
  34. data/lib/bunto/entry_filter.rb +72 -0
  35. data/lib/bunto/errors.rb +10 -0
  36. data/lib/bunto/excerpt.rb +127 -0
  37. data/lib/bunto/external.rb +59 -0
  38. data/lib/bunto/filters.rb +367 -0
  39. data/lib/bunto/frontmatter_defaults.rb +188 -0
  40. data/lib/bunto/generator.rb +3 -0
  41. data/lib/bunto/hooks.rb +101 -0
  42. data/lib/bunto/layout.rb +49 -0
  43. data/lib/bunto/liquid_extensions.rb +22 -0
  44. data/lib/bunto/liquid_renderer.rb +39 -0
  45. data/lib/bunto/liquid_renderer/file.rb +50 -0
  46. data/lib/bunto/liquid_renderer/table.rb +94 -0
  47. data/lib/bunto/log_adapter.rb +115 -0
  48. data/lib/bunto/mime.types +800 -0
  49. data/lib/bunto/page.rb +180 -0
  50. data/lib/bunto/plugin.rb +96 -0
  51. data/lib/bunto/plugin_manager.rb +95 -0
  52. data/lib/bunto/post.rb +329 -0
  53. data/lib/bunto/publisher.rb +21 -0
  54. data/lib/bunto/reader.rb +126 -0
  55. data/lib/bunto/readers/collection_reader.rb +20 -0
  56. data/lib/bunto/readers/data_reader.rb +69 -0
  57. data/lib/bunto/readers/layout_reader.rb +53 -0
  58. data/lib/bunto/readers/page_reader.rb +21 -0
  59. data/lib/bunto/readers/post_reader.rb +62 -0
  60. data/lib/bunto/readers/static_file_reader.rb +21 -0
  61. data/lib/bunto/regenerator.rb +175 -0
  62. data/lib/bunto/related_posts.rb +56 -0
  63. data/lib/bunto/renderer.rb +191 -0
  64. data/lib/bunto/site.rb +391 -0
  65. data/lib/bunto/static_file.rb +141 -0
  66. data/lib/bunto/stevenson.rb +58 -0
  67. data/lib/bunto/tags/highlight.rb +122 -0
  68. data/lib/bunto/tags/include.rb +190 -0
  69. data/lib/bunto/tags/post_url.rb +88 -0
  70. data/lib/bunto/url.rb +136 -0
  71. data/lib/bunto/utils.rb +287 -0
  72. data/lib/bunto/utils/ansi.rb +59 -0
  73. data/lib/bunto/utils/platforms.rb +30 -0
  74. data/lib/bunto/version.rb +3 -0
  75. data/lib/site_template/.gitignore +3 -0
  76. data/lib/site_template/_config.yml +21 -0
  77. data/lib/site_template/_includes/footer.html +38 -0
  78. data/lib/site_template/_includes/head.html +12 -0
  79. data/lib/site_template/_includes/header.html +27 -0
  80. data/lib/site_template/_includes/icon-github.html +1 -0
  81. data/lib/site_template/_includes/icon-github.svg +1 -0
  82. data/lib/site_template/_includes/icon-twitter.html +1 -0
  83. data/lib/site_template/_includes/icon-twitter.svg +1 -0
  84. data/lib/site_template/_layouts/default.html +20 -0
  85. data/lib/site_template/_layouts/page.html +14 -0
  86. data/lib/site_template/_layouts/post.html +15 -0
  87. data/lib/site_template/_posts/0000-00-00-welcome-to-bunto.markdown.erb +25 -0
  88. data/lib/site_template/_sass/_base.scss +206 -0
  89. data/lib/site_template/_sass/_layout.scss +242 -0
  90. data/lib/site_template/_sass/_syntax-highlighting.scss +71 -0
  91. data/lib/site_template/about.md +15 -0
  92. data/lib/site_template/css/main.scss +53 -0
  93. data/lib/site_template/feed.xml +30 -0
  94. data/lib/site_template/index.html +23 -0
  95. metadata +252 -0
@@ -0,0 +1,141 @@
1
+ module Bunto
2
+ class StaticFile
3
+ # The cache of last modification times [path] -> mtime.
4
+ @@mtimes = {}
5
+
6
+ attr_reader :relative_path, :extname
7
+
8
+ # Initialize a new StaticFile.
9
+ #
10
+ # site - The Site.
11
+ # base - The String path to the <source>.
12
+ # dir - The String path between <source> and the file.
13
+ # name - The String filename of the file.
14
+ def initialize(site, base, dir, name, collection = nil)
15
+ @site = site
16
+ @base = base
17
+ @dir = dir
18
+ @name = name
19
+ @collection = collection
20
+ @relative_path = File.join(*[@dir, @name].compact)
21
+ @extname = File.extname(@name)
22
+ end
23
+
24
+ # Returns source file path.
25
+ def path
26
+ File.join(*[@base, @dir, @name].compact)
27
+ end
28
+
29
+ # Obtain destination path.
30
+ #
31
+ # dest - The String path to the destination dir.
32
+ #
33
+ # Returns destination file path.
34
+ def destination(dest)
35
+ @site.in_dest_dir(*[dest, destination_rel_dir, @name].compact)
36
+ end
37
+
38
+ def destination_rel_dir
39
+ if @collection
40
+ File.dirname(url)
41
+ else
42
+ @dir
43
+ end
44
+ end
45
+
46
+ def modified_time
47
+ @modified_time ||= File.stat(path).mtime
48
+ end
49
+
50
+ # Returns last modification time for this file.
51
+ def mtime
52
+ modified_time.to_i
53
+ end
54
+
55
+ # Is source path modified?
56
+ #
57
+ # Returns true if modified since last write.
58
+ def modified?
59
+ @@mtimes[path] != mtime
60
+ end
61
+
62
+ # Whether to write the file to the filesystem
63
+ #
64
+ # Returns true unless the defaults for the destination path from
65
+ # _config.yml contain `published: false`.
66
+ def write?
67
+ defaults.fetch('published', true)
68
+ end
69
+
70
+ # Write the static file to the destination directory (if modified).
71
+ #
72
+ # dest - The String path to the destination dir.
73
+ #
74
+ # Returns false if the file was not modified since last time (no-op).
75
+ def write(dest)
76
+ dest_path = destination(dest)
77
+
78
+ return false if File.exist?(dest_path) && !modified?
79
+ @@mtimes[path] = mtime
80
+
81
+ FileUtils.mkdir_p(File.dirname(dest_path))
82
+ FileUtils.rm(dest_path) if File.exist?(dest_path)
83
+ FileUtils.cp(path, dest_path)
84
+ File.utime(@@mtimes[path], @@mtimes[path], dest_path)
85
+
86
+ true
87
+ end
88
+
89
+ # Reset the mtimes cache (for testing purposes).
90
+ #
91
+ # Returns nothing.
92
+ def self.reset_cache
93
+ @@mtimes = {}
94
+ nil
95
+ end
96
+
97
+ def to_liquid
98
+ {
99
+ "extname" => extname,
100
+ "modified_time" => modified_time,
101
+ "path" => File.join("", relative_path)
102
+ }
103
+ end
104
+
105
+ def placeholders
106
+ {
107
+ :collection => @collection.label,
108
+ :path => relative_path[
109
+ @collection.relative_directory.size..relative_path.size],
110
+ :output_ext => '',
111
+ :name => '',
112
+ :title => ''
113
+ }
114
+ end
115
+
116
+ # Applies a similar URL-building technique as Bunto::Document that takes
117
+ # the collection's URL template into account. The default URL template can
118
+ # be overriden in the collection's configuration in _config.yml.
119
+ def url
120
+ @url ||= if @collection.nil?
121
+ relative_path
122
+ else
123
+ ::Bunto::URL.new({
124
+ :template => @collection.url_template,
125
+ :placeholders => placeholders
126
+ })
127
+ end.to_s.gsub(/\/$/, '')
128
+ end
129
+
130
+ # Returns the type of the collection if present, nil otherwise.
131
+ def type
132
+ @type ||= @collection.nil? ? nil : @collection.label.to_sym
133
+ end
134
+
135
+ # Returns the front matter defaults defined for the file's URL and/or type
136
+ # as defined in _config.yml.
137
+ def defaults
138
+ @defaults ||= @site.frontmatter_defaults.all url, type
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,58 @@
1
+ module Bunto
2
+ class Stevenson < ::Logger
3
+ def initialize
4
+ @progname = nil
5
+ @level = DEBUG
6
+ @default_formatter = Formatter.new
7
+ @logdev = $stdout
8
+ @formatter = proc do |_, _, _, msg|
9
+ "#{msg}"
10
+ end
11
+ end
12
+
13
+ def add(severity, message = nil, progname = nil, &block)
14
+ severity ||= UNKNOWN
15
+ @logdev = set_logdevice(severity)
16
+
17
+ if @logdev.nil? || severity < @level
18
+ return true
19
+ end
20
+ progname ||= @progname
21
+ if message.nil?
22
+ if block_given?
23
+ message = yield
24
+ else
25
+ message = progname
26
+ progname = @progname
27
+ end
28
+ end
29
+ @logdev.puts(
30
+ format_message(format_severity(severity), Time.now, progname, message))
31
+ true
32
+ end
33
+
34
+ # Log a +WARN+ message
35
+ def warn(progname = nil, &block)
36
+ add(WARN, nil, progname.yellow, &block)
37
+ end
38
+
39
+ # Log an +ERROR+ message
40
+ def error(progname = nil, &block)
41
+ add(ERROR, nil, progname.red, &block)
42
+ end
43
+
44
+ def close
45
+ # No LogDevice in use
46
+ end
47
+
48
+ private
49
+
50
+ def set_logdevice(severity)
51
+ if severity > INFO
52
+ $stderr
53
+ else
54
+ $stdout
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,122 @@
1
+ module Bunto
2
+ module Tags
3
+ class HighlightBlock < Liquid::Block
4
+ include Liquid::StandardFilters
5
+
6
+ # The regular expression syntax checker. Start with the language specifier.
7
+ # Follow that by zero or more space separated options that take one of three
8
+ # forms: name, name=value, or name="<quoted list>"
9
+ #
10
+ # <quoted list> is a space-separated list of numbers
11
+ SYNTAX = /^([a-zA-Z0-9.+#-]+)((\s+\w+(=(\w+|"([0-9]+\s)*[0-9]+"))?)*)$/
12
+
13
+ def initialize(tag_name, markup, tokens)
14
+ super
15
+ if markup.strip =~ SYNTAX
16
+ @lang = Regexp.last_match(1).downcase
17
+ @highlight_options = {}
18
+ if defined?(Regexp.last_match(2)) && Regexp.last_match(2) != ''
19
+ # Split along 3 possible forms -- key="<quoted list>", key=value, or key
20
+ Regexp.last_match(2).scan(/(?:\w="[^"]*"|\w=\w|\w)+/) do |opt|
21
+ key, value = opt.split('=')
22
+ # If a quoted list, convert to array
23
+ if value && value.include?("\"")
24
+ value.delete!('"')
25
+ value = value.split
26
+ end
27
+ @highlight_options[key.to_sym] = value || true
28
+ end
29
+ end
30
+ @highlight_options[:linenos] = "inline" if @highlight_options.key?(:linenos) && @highlight_options[:linenos] == true
31
+ else
32
+ raise SyntaxError.new <<-eos
33
+ Syntax Error in tag 'highlight' while parsing the following markup:
34
+
35
+ #{markup}
36
+
37
+ Valid syntax: highlight <lang> [linenos]
38
+ eos
39
+ end
40
+ end
41
+
42
+ def render(context)
43
+ prefix = context["highlighter_prefix"] || ""
44
+ suffix = context["highlighter_suffix"] || ""
45
+ code = super.to_s.gsub(/\A(\n|\r)+|(\n|\r)+\z/, '')
46
+
47
+ is_safe = !!context.registers[:site].safe
48
+
49
+ output =
50
+ case context.registers[:site].highlighter
51
+ when 'pygments'
52
+ render_pygments(code, is_safe)
53
+ when 'rouge'
54
+ render_rouge(code)
55
+ else
56
+ render_codehighlighter(code)
57
+ end
58
+
59
+ rendered_output = add_code_tag(output)
60
+ prefix + rendered_output + suffix
61
+ end
62
+
63
+ def sanitized_opts(opts, is_safe)
64
+ if is_safe
65
+ Hash[[
66
+ [:startinline, opts.fetch(:startinline, nil)],
67
+ [:hl_lines, opts.fetch(:hl_lines, nil)],
68
+ [:linenos, opts.fetch(:linenos, nil)],
69
+ [:encoding, opts.fetch(:encoding, 'utf-8')],
70
+ [:cssclass, opts.fetch(:cssclass, nil)]
71
+ ].reject { |f| f.last.nil? }]
72
+ else
73
+ opts
74
+ end
75
+ end
76
+
77
+ def render_pygments(code, is_safe)
78
+ Bunto::External.require_with_graceful_fail('pygments')
79
+
80
+ highlighted_code = Pygments.highlight(
81
+ code,
82
+ :lexer => @lang,
83
+ :options => sanitized_opts(@highlight_options, is_safe)
84
+ )
85
+
86
+ if highlighted_code.nil?
87
+ Bunto.logger.error "There was an error highlighting your code:"
88
+ puts
89
+ Bunto.logger.error code
90
+ puts
91
+ Bunto.logger.error "While attempting to convert the above code, Pygments.rb" \
92
+ " returned an unacceptable value."
93
+ Bunto.logger.error "This is usually a timeout problem solved by running `bunto build` again."
94
+ raise ArgumentError.new("Pygments.rb returned an unacceptable value when attempting to highlight some code.")
95
+ end
96
+
97
+ highlighted_code.sub('<div class="highlight"><pre>', '').sub('</pre></div>', '')
98
+ end
99
+
100
+ def render_rouge(code)
101
+ Bunto::External.require_with_graceful_fail('rouge')
102
+ formatter = Rouge::Formatters::HTML.new(:line_numbers => @highlight_options[:linenos], :wrap => false)
103
+ lexer = Rouge::Lexer.find_fancy(@lang, code) || Rouge::Lexers::PlainText
104
+ formatter.format(lexer.lex(code))
105
+ end
106
+
107
+ def render_codehighlighter(code)
108
+ h(code).strip
109
+ end
110
+
111
+ def add_code_tag(code)
112
+ code_attributes = [
113
+ "class=\"language-#{@lang.to_s.tr('+', '-')}\"",
114
+ "data-lang=\"#{@lang}\""
115
+ ].join(" ")
116
+ "<figure class=\"highlight\"><pre><code #{code_attributes}>#{code.chomp}</code></pre></figure>"
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ Liquid::Template.register_tag('highlight', Bunto::Tags::HighlightBlock)
@@ -0,0 +1,190 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunto
4
+ module Tags
5
+ class IncludeTagError < StandardError
6
+ attr_accessor :path
7
+
8
+ def initialize(msg, path)
9
+ super(msg)
10
+ @path = path
11
+ end
12
+ end
13
+
14
+ class IncludeTag < Liquid::Tag
15
+ attr_reader :includes_dir
16
+
17
+ VALID_SYNTAX = /([\w-]+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))/
18
+ VARIABLE_SYNTAX = /(?<variable>[^{]*(\{\{\s*[\w\-\.]+\s*(\|.*)?\}\}[^\s{}]*)+)(?<params>.*)/
19
+
20
+ def initialize(tag_name, markup, tokens)
21
+ super
22
+ matched = markup.strip.match(VARIABLE_SYNTAX)
23
+ if matched
24
+ @file = matched['variable'].strip
25
+ @params = matched['params'].strip
26
+ else
27
+ @file, @params = markup.strip.split(' ', 2)
28
+ end
29
+ validate_params if @params
30
+ @tag_name = tag_name
31
+ end
32
+
33
+ def syntax_example
34
+ "{% #{@tag_name} file.ext param='value' param2='value' %}"
35
+ end
36
+
37
+ def parse_params(context)
38
+ params = {}
39
+ markup = @params
40
+
41
+ while match = VALID_SYNTAX.match(markup) do
42
+ markup = markup[match.end(0)..-1]
43
+
44
+ value = if match[2]
45
+ match[2].gsub(/\\"/, '"')
46
+ elsif match[3]
47
+ match[3].gsub(/\\'/, "'")
48
+ elsif match[4]
49
+ context[match[4]]
50
+ end
51
+
52
+ params[match[1]] = value
53
+ end
54
+ params
55
+ end
56
+
57
+ def validate_file_name(file)
58
+ if file !~ /^[a-zA-Z0-9_\/\.-]+$/ || file =~ /\.\// || file =~ /\/\./
59
+ raise ArgumentError.new <<-eos
60
+ Invalid syntax for include tag. File contains invalid characters or sequences:
61
+
62
+ #{file}
63
+
64
+ Valid syntax:
65
+
66
+ #{syntax_example}
67
+
68
+ eos
69
+ end
70
+ end
71
+
72
+ def validate_params
73
+ full_valid_syntax = Regexp.compile('\A\s*(?:' + VALID_SYNTAX.to_s + '(?=\s|\z)\s*)*\z')
74
+ unless @params =~ full_valid_syntax
75
+ raise ArgumentError.new <<-eos
76
+ Invalid syntax for include tag:
77
+
78
+ #{@params}
79
+
80
+ Valid syntax:
81
+
82
+ #{syntax_example}
83
+
84
+ eos
85
+ end
86
+ end
87
+
88
+ # Grab file read opts in the context
89
+ def file_read_opts(context)
90
+ context.registers[:site].file_read_opts
91
+ end
92
+
93
+ # Render the variable if required
94
+ def render_variable(context)
95
+ if @file.match(VARIABLE_SYNTAX)
96
+ partial = context.registers[:site].liquid_renderer.file("(variable)").parse(@file)
97
+ partial.render!(context)
98
+ end
99
+ end
100
+
101
+ def tag_includes_dir(context)
102
+ context.registers[:site].config['includes_dir'].freeze
103
+ end
104
+
105
+ def render(context)
106
+ site = context.registers[:site]
107
+ @includes_dir = tag_includes_dir(context)
108
+ dir = resolved_includes_dir(context)
109
+
110
+ file = render_variable(context) || @file
111
+ validate_file_name(file)
112
+
113
+ path = File.join(dir, file)
114
+ validate_path(path, dir, site.safe)
115
+
116
+ # Add include to dependency tree
117
+ if context.registers[:page] && context.registers[:page].key?("path")
118
+ site.regenerator.add_dependency(
119
+ site.in_source_dir(context.registers[:page]["path"]),
120
+ path
121
+ )
122
+ end
123
+
124
+ begin
125
+ partial = load_cached_partial(path, context)
126
+
127
+ context.stack do
128
+ context['include'] = parse_params(context) if @params
129
+ partial.render!(context)
130
+ end
131
+ rescue => e
132
+ raise IncludeTagError.new e.message, File.join(@includes_dir, @file)
133
+ end
134
+ end
135
+
136
+ def load_cached_partial(path, context)
137
+ context.registers[:cached_partials] ||= {}
138
+ cached_partial = context.registers[:cached_partials]
139
+
140
+ if cached_partial.key?(path)
141
+ cached_partial[path]
142
+ else
143
+ cached_partial[path] = context.registers[:site].liquid_renderer.file(path).parse(read_file(path, context))
144
+ end
145
+ end
146
+
147
+ def resolved_includes_dir(context)
148
+ context.registers[:site].in_source_dir(@includes_dir)
149
+ end
150
+
151
+ def validate_path(path, dir, safe)
152
+ if safe && !realpath_prefixed_with?(path, dir)
153
+ raise IOError.new "The included file '#{path}' should exist and should not be a symlink"
154
+ elsif !File.exist?(path)
155
+ raise IOError.new "Included file '#{path_relative_to_source(dir, path)}' not found"
156
+ end
157
+ end
158
+
159
+ def path_relative_to_source(dir, path)
160
+ File.join(@includes_dir, path.sub(Regexp.new("^#{dir}"), ""))
161
+ end
162
+
163
+ def realpath_prefixed_with?(path, dir)
164
+ File.exist?(path) && File.realpath(path).start_with?(dir)
165
+ end
166
+
167
+ # This method allows to modify the file content by inheriting from the class.
168
+ def read_file(file, context)
169
+ File.read(file, file_read_opts(context))
170
+ end
171
+ end
172
+
173
+ class IncludeRelativeTag < IncludeTag
174
+ def tag_includes_dir(context)
175
+ '.'.freeze
176
+ end
177
+
178
+ def page_path(context)
179
+ context.registers[:page].nil? ? includes_dir : File.dirname(context.registers[:page]["path"])
180
+ end
181
+
182
+ def resolved_includes_dir(context)
183
+ context.registers[:site].in_source_dir(page_path(context))
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ Liquid::Template.register_tag('include', Bunto::Tags::IncludeTag)
190
+ Liquid::Template.register_tag('include_relative', Bunto::Tags::IncludeRelativeTag)