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,46 @@
1
+ module Bunto
2
+ module Deprecator
3
+ extend self
4
+
5
+ def process(args)
6
+ arg_is_present? args, "--server", "The --server command has been replaced by the \
7
+ 'serve' subcommand."
8
+ arg_is_present? args, "--serve", "The --server command has been replaced by the \
9
+ 'serve' subcommand."
10
+ arg_is_present? args, "--no-server", "To build Bunto without launching a server, \
11
+ use the 'build' subcommand."
12
+ arg_is_present? args, "--auto", "The switch '--auto' has been replaced with '--watch'."
13
+ arg_is_present? args, "--no-auto", "To disable auto-replication, simply leave off \
14
+ the '--watch' switch."
15
+ arg_is_present? args, "--pygments", "The 'pygments'settings has been removed in \
16
+ favour of 'highlighter'."
17
+ arg_is_present? args, "--paginate", "The 'paginate' setting can only be set in your \
18
+ config files."
19
+ arg_is_present? args, "--url", "The 'url' setting can only be set in your config files."
20
+ no_subcommand(args)
21
+ end
22
+
23
+ def no_subcommand(args)
24
+ if args.size > 0 && args.first =~ /^--/ && !%w(--help --version).include?(args.first)
25
+ deprecation_message "Bunto now uses subcommands instead of just switches. Run `bunto --help` to find out more."
26
+ abort
27
+ end
28
+ end
29
+
30
+ def arg_is_present?(args, deprecated_argument, message)
31
+ if args.include?(deprecated_argument)
32
+ deprecation_message(message)
33
+ end
34
+ end
35
+
36
+ def deprecation_message(message)
37
+ Bunto.logger.error "Deprecation:", message
38
+ end
39
+
40
+ def defaults_deprecate_type(old, current)
41
+ Bunto.logger.warn "Defaults:", "The '#{old}' type has become '#{current}'."
42
+ Bunto.logger.warn "Defaults:", "Please update your front-matter defaults to use 'type: #{current}'."
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,444 @@
1
+ # encoding: UTF-8
2
+
3
+ module Bunto
4
+ class Document
5
+ include Comparable
6
+
7
+ attr_reader :path, :site, :extname, :collection
8
+ attr_accessor :content, :output
9
+
10
+ YAML_FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
11
+ DATELESS_FILENAME_MATCHER = /^(.+\/)*(.*)(\.[^.]+)$/
12
+ DATE_FILENAME_MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/
13
+
14
+ # Create a new Document.
15
+ #
16
+ # path - the path to the file
17
+ # relations - a hash with keys :site and :collection, the values of which
18
+ # are the Bunto::Site and Bunto::Collection to which this
19
+ # Document belong.
20
+ #
21
+ # Returns nothing.
22
+ def initialize(path, relations = {})
23
+ @site = relations[:site]
24
+ @path = path
25
+ @extname = File.extname(path)
26
+ @collection = relations[:collection]
27
+ @has_yaml_header = nil
28
+
29
+ if draft?
30
+ categories_from_path("_drafts")
31
+ else
32
+ categories_from_path(collection.relative_directory)
33
+ end
34
+
35
+ data.default_proc = proc do |_, key|
36
+ site.frontmatter_defaults.find(relative_path, collection.label, key)
37
+ end
38
+
39
+ trigger_hooks(:post_init)
40
+ end
41
+
42
+ # Fetch the Document's data.
43
+ #
44
+ # Returns a Hash containing the data. An empty hash is returned if
45
+ # no data was read.
46
+ def data
47
+ @data ||= {}
48
+ end
49
+
50
+ # Merge some data in with this document's data.
51
+ #
52
+ # Returns the merged data.
53
+ def merge_data!(other, source: "YAML front matter")
54
+ if other.key?('categories') && !other['categories'].nil?
55
+ if other['categories'].is_a?(String)
56
+ other['categories'] = other['categories'].split(" ").map(&:strip)
57
+ end
58
+ other['categories'] = (data['categories'] || []) | other['categories']
59
+ end
60
+ Utils.deep_merge_hashes!(data, other)
61
+ if data.key?('date') && !data['date'].is_a?(Time)
62
+ data['date'] = Utils.parse_date(
63
+ data['date'].to_s,
64
+ "Document '#{relative_path}' does not have a valid date in the #{source}."
65
+ )
66
+ end
67
+ data
68
+ end
69
+
70
+ def date
71
+ data['date'] ||= site.time
72
+ end
73
+
74
+ # Returns whether the document is a draft. This is only the case if
75
+ # the document is in the 'posts' collection but in a different
76
+ # directory than '_posts'.
77
+ #
78
+ # Returns whether the document is a draft.
79
+ def draft?
80
+ data['draft'] ||= relative_path.index(collection.relative_directory).nil? && collection.label == "posts"
81
+ end
82
+
83
+ # The path to the document, relative to the site source.
84
+ #
85
+ # Returns a String path which represents the relative path
86
+ # from the site source to this document
87
+ def relative_path
88
+ @relative_path ||= Pathname.new(path).relative_path_from(Pathname.new(site.source)).to_s
89
+ end
90
+
91
+ # The output extension of the document.
92
+ #
93
+ # Returns the output extension
94
+ def output_ext
95
+ Bunto::Renderer.new(site, self).output_ext
96
+ end
97
+
98
+ # The base filename of the document, without the file extname.
99
+ #
100
+ # Returns the basename without the file extname.
101
+ def basename_without_ext
102
+ @basename_without_ext ||= File.basename(path, '.*')
103
+ end
104
+
105
+ # The base filename of the document.
106
+ #
107
+ # Returns the base filename of the document.
108
+ def basename
109
+ @basename ||= File.basename(path)
110
+ end
111
+
112
+ # Produces a "cleaned" relative path.
113
+ # The "cleaned" relative path is the relative path without the extname
114
+ # and with the collection's directory removed as well.
115
+ # This method is useful when building the URL of the document.
116
+ #
117
+ # Examples:
118
+ # When relative_path is "_methods/site/generate.md":
119
+ # cleaned_relative_path
120
+ # # => "/site/generate"
121
+ #
122
+ # Returns the cleaned relative path of the document.
123
+ def cleaned_relative_path
124
+ @cleaned_relative_path ||=
125
+ relative_path[0..-extname.length - 1].sub(collection.relative_directory, "")
126
+ end
127
+
128
+ # Determine whether the document is a YAML file.
129
+ #
130
+ # Returns true if the extname is either .yml or .yaml, false otherwise.
131
+ def yaml_file?
132
+ %w(.yaml .yml).include?(extname)
133
+ end
134
+
135
+ # Determine whether the document is an asset file.
136
+ # Asset files include CoffeeScript files and Sass/SCSS files.
137
+ #
138
+ # Returns true if the extname belongs to the set of extensions
139
+ # that asset files use.
140
+ def asset_file?
141
+ sass_file? || coffeescript_file?
142
+ end
143
+
144
+ # Determine whether the document is a Sass file.
145
+ #
146
+ # Returns true if extname == .sass or .scss, false otherwise.
147
+ def sass_file?
148
+ %w(.sass .scss).include?(extname)
149
+ end
150
+
151
+ # Determine whether the document is a CoffeeScript file.
152
+ #
153
+ # Returns true if extname == .coffee, false otherwise.
154
+ def coffeescript_file?
155
+ '.coffee'.eql?(extname)
156
+ end
157
+
158
+ # Determine whether the file should be rendered with Liquid.
159
+ #
160
+ # Returns false if the document is either an asset file or a yaml file,
161
+ # true otherwise.
162
+ def render_with_liquid?
163
+ !(coffeescript_file? || yaml_file?)
164
+ end
165
+
166
+ # Determine whether the file should be placed into layouts.
167
+ #
168
+ # Returns false if the document is either an asset file or a yaml file,
169
+ # true otherwise.
170
+ def place_in_layout?
171
+ !(asset_file? || yaml_file?)
172
+ end
173
+
174
+ # The URL template where the document would be accessible.
175
+ #
176
+ # Returns the URL template for the document.
177
+ def url_template
178
+ collection.url_template
179
+ end
180
+
181
+ # Construct a Hash of key-value pairs which contain a mapping between
182
+ # a key in the URL template and the corresponding value for this document.
183
+ #
184
+ # Returns the Hash of key-value pairs for replacement in the URL.
185
+ def url_placeholders
186
+ @url_placeholders ||= Drops::UrlDrop.new(self)
187
+ end
188
+
189
+ # The permalink for this Document.
190
+ # Permalink is set via the data Hash.
191
+ #
192
+ # Returns the permalink or nil if no permalink was set in the data.
193
+ def permalink
194
+ data && data.is_a?(Hash) && data['permalink']
195
+ end
196
+
197
+ # The computed URL for the document. See `Bunto::URL#to_s` for more details.
198
+ #
199
+ # Returns the computed URL for the document.
200
+ def url
201
+ @url = URL.new({
202
+ :template => url_template,
203
+ :placeholders => url_placeholders,
204
+ :permalink => permalink
205
+ }).to_s
206
+ end
207
+
208
+ def [](key)
209
+ data[key]
210
+ end
211
+
212
+ # The full path to the output file.
213
+ #
214
+ # base_directory - the base path of the output directory
215
+ #
216
+ # Returns the full path to the output file of this document.
217
+ def destination(base_directory)
218
+ dest = site.in_dest_dir(base_directory)
219
+ path = site.in_dest_dir(dest, URL.unescape_path(url))
220
+ path = File.join(path, "index.html") if url.end_with?("/")
221
+ path << output_ext unless path.end_with? output_ext
222
+ path
223
+ end
224
+
225
+ # Write the generated Document file to the destination directory.
226
+ #
227
+ # dest - The String path to the destination dir.
228
+ #
229
+ # Returns nothing.
230
+ def write(dest)
231
+ path = destination(dest)
232
+ FileUtils.mkdir_p(File.dirname(path))
233
+ File.open(path, 'wb') do |f|
234
+ f.write(output)
235
+ end
236
+
237
+ trigger_hooks(:post_write)
238
+ end
239
+
240
+ # Whether the file is published or not, as indicated in YAML front-matter
241
+ #
242
+ # Returns true if the 'published' key is specified in the YAML front-matter and not `false`.
243
+ def published?
244
+ !(data.key?('published') && data['published'] == false)
245
+ end
246
+
247
+ # Read in the file and assign the content and data based on the file contents.
248
+ # Merge the frontmatter of the file with the frontmatter default
249
+ # values
250
+ #
251
+ # Returns nothing.
252
+ def read(opts = {})
253
+ Bunto.logger.debug "Reading:", relative_path
254
+
255
+ if yaml_file?
256
+ @data = SafeYAML.load_file(path)
257
+ else
258
+ begin
259
+ defaults = @site.frontmatter_defaults.all(url, collection.label.to_sym)
260
+ merge_data!(defaults, source: "front matter defaults") unless defaults.empty?
261
+
262
+ self.content = File.read(path, Utils.merged_file_read_opts(site, opts))
263
+ if content =~ YAML_FRONT_MATTER_REGEXP
264
+ self.content = $POSTMATCH
265
+ data_file = SafeYAML.load(Regexp.last_match(1))
266
+ merge_data!(data_file, source: "YAML front matter") if data_file
267
+ end
268
+
269
+ post_read
270
+ rescue SyntaxError => e
271
+ Bunto.logger.error "Error:", "YAML Exception reading #{path}: #{e.message}"
272
+ rescue Exception => e
273
+ if e.is_a? Bunto::Errors::FatalException
274
+ raise e
275
+ end
276
+ Bunto.logger.error "Error:", "could not read file #{path}: #{e.message}"
277
+ end
278
+ end
279
+ end
280
+
281
+ def post_read
282
+ if relative_path =~ DATE_FILENAME_MATCHER
283
+ date, slug, ext = $2, $3, $4
284
+ if !data['date'] || data['date'].to_i == site.time.to_i
285
+ merge_data!({"date" => date}, source: "filename")
286
+ end
287
+ elsif relative_path =~ DATELESS_FILENAME_MATCHER
288
+ slug, ext = $2, $3
289
+ end
290
+
291
+ # Try to ensure the user gets a title.
292
+ data["title"] ||= Utils.titleize_slug(slug)
293
+ # Only overwrite slug & ext if they aren't specified.
294
+ data['slug'] ||= slug
295
+ data['ext'] ||= ext
296
+
297
+ populate_categories
298
+ populate_tags
299
+ generate_excerpt
300
+ end
301
+
302
+ # Add superdirectories of the special_dir to categories.
303
+ # In the case of es/_posts, 'es' is added as a category.
304
+ # In the case of _posts/es, 'es' is NOT added as a category.
305
+ #
306
+ # Returns nothing.
307
+ def categories_from_path(special_dir)
308
+ superdirs = relative_path.sub(/#{special_dir}(.*)/, '').split(File::SEPARATOR).reject do |c|
309
+ c.empty? || c.eql?(special_dir) || c.eql?(basename)
310
+ end
311
+ merge_data!({ 'categories' => superdirs }, source: "file path")
312
+ end
313
+
314
+ def populate_categories
315
+ merge_data!({
316
+ 'categories' => (
317
+ Array(data['categories']) + Utils.pluralized_array_from_hash(data, 'category', 'categories')
318
+ ).map(&:to_s).flatten.uniq
319
+ })
320
+ end
321
+
322
+ def populate_tags
323
+ merge_data!({
324
+ "tags" => Utils.pluralized_array_from_hash(data, "tag", "tags").flatten
325
+ })
326
+ end
327
+
328
+ # Create a Liquid-understandable version of this Document.
329
+ #
330
+ # Returns a Hash representing this Document's data.
331
+ def to_liquid
332
+ @to_liquid ||= Drops::DocumentDrop.new(self)
333
+ end
334
+
335
+ # The inspect string for this document.
336
+ # Includes the relative path and the collection label.
337
+ #
338
+ # Returns the inspect string for this document.
339
+ def inspect
340
+ "#<Bunto::Document #{relative_path} collection=#{collection.label}>"
341
+ end
342
+
343
+ # The string representation for this document.
344
+ #
345
+ # Returns the content of the document
346
+ def to_s
347
+ output || content || 'NO CONTENT'
348
+ end
349
+
350
+ # Compare this document against another document.
351
+ # Comparison is a comparison between the 2 paths of the documents.
352
+ #
353
+ # Returns -1, 0, +1 or nil depending on whether this doc's path is less than,
354
+ # equal or greater than the other doc's path. See String#<=> for more details.
355
+ def <=>(other)
356
+ return nil unless other.respond_to?(:data)
357
+ cmp = data['date'] <=> other.data['date']
358
+ cmp = path <=> other.path if cmp.nil? || cmp == 0
359
+ cmp
360
+ end
361
+
362
+ # Determine whether this document should be written.
363
+ # Based on the Collection to which it belongs.
364
+ #
365
+ # True if the document has a collection and if that collection's #write?
366
+ # method returns true, otherwise false.
367
+ def write?
368
+ collection && collection.write?
369
+ end
370
+
371
+ # The Document excerpt_separator, from the YAML Front-Matter or site
372
+ # default excerpt_separator value
373
+ #
374
+ # Returns the document excerpt_separator
375
+ def excerpt_separator
376
+ (data['excerpt_separator'] || site.config['excerpt_separator']).to_s
377
+ end
378
+
379
+ # Whether to generate an excerpt
380
+ #
381
+ # Returns true if the excerpt separator is configured.
382
+ def generate_excerpt?
383
+ !excerpt_separator.empty?
384
+ end
385
+
386
+ def next_doc
387
+ pos = collection.docs.index { |post| post.equal?(self) }
388
+ if pos && pos < collection.docs.length - 1
389
+ collection.docs[pos + 1]
390
+ else
391
+ nil
392
+ end
393
+ end
394
+
395
+ def previous_doc
396
+ pos = collection.docs.index { |post| post.equal?(self) }
397
+ if pos && pos > 0
398
+ collection.docs[pos - 1]
399
+ else
400
+ nil
401
+ end
402
+ end
403
+
404
+ def trigger_hooks(hook_name, *args)
405
+ Bunto::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection
406
+ Bunto::Hooks.trigger :documents, hook_name, self, *args
407
+ end
408
+
409
+ def id
410
+ @id ||= File.join(File.dirname(url), (data['slug'] || basename_without_ext).to_s)
411
+ end
412
+
413
+ # Calculate related posts.
414
+ #
415
+ # Returns an Array of related Posts.
416
+ def related_posts
417
+ Bunto::RelatedPosts.new(self).build
418
+ end
419
+
420
+ # Override of normal respond_to? to match method_missing's logic for
421
+ # looking in @data.
422
+ def respond_to?(method, include_private = false)
423
+ data.key?(method.to_s) || super
424
+ end
425
+
426
+ # Override of method_missing to check in @data for the key.
427
+ def method_missing(method, *args, &blck)
428
+ if data.key?(method.to_s)
429
+ Bunto.logger.warn "Deprecation:", "Document##{method} is now a key in the #data hash."
430
+ Bunto.logger.warn "", "Called by #{caller.first}."
431
+ data[method.to_s]
432
+ else
433
+ super
434
+ end
435
+ end
436
+
437
+ private # :nodoc:
438
+ def generate_excerpt
439
+ if generate_excerpt?
440
+ data["excerpt"] ||= Bunto::Excerpt.new(self)
441
+ end
442
+ end
443
+ end
444
+ end