bunto 1.0.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 (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)