henshin 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +5 -1
  2. data/README.markdown +9 -46
  3. data/Rakefile +2 -1
  4. data/VERSION +1 -1
  5. data/bin/henshin +11 -12
  6. data/henshin.gemspec +20 -27
  7. data/lib/henshin.rb +21 -98
  8. data/lib/henshin/archive.rb +87 -115
  9. data/{bin → lib/henshin/exec}/files.rb +0 -0
  10. data/lib/henshin/ext.rb +35 -58
  11. data/lib/henshin/gen.rb +83 -68
  12. data/lib/henshin/labels.rb +144 -0
  13. data/lib/henshin/plugin.rb +70 -33
  14. data/lib/henshin/plugins/highlight.rb +19 -26
  15. data/lib/henshin/plugins/liquid.rb +50 -52
  16. data/lib/henshin/plugins/maruku.rb +14 -16
  17. data/lib/henshin/plugins/sass.rb +19 -23
  18. data/lib/henshin/plugins/textile.rb +14 -16
  19. data/lib/henshin/post.rb +67 -120
  20. data/lib/henshin/site.rb +154 -131
  21. data/lib/henshin/static.rb +10 -8
  22. data/test/helper.rb +20 -8
  23. data/test/site/css/{includes/reset.sass → _reset.sass} +0 -0
  24. data/test/site/css/screen.sass +1 -1
  25. data/test/site/index.html +2 -3
  26. data/test/site/layouts/archive_date.html +5 -4
  27. data/test/site/layouts/archive_month.html +10 -5
  28. data/test/site/layouts/archive_year.html +13 -6
  29. data/test/site/layouts/category_page.html +7 -7
  30. data/test/site/layouts/main.html +3 -4
  31. data/test/site/layouts/post.html +1 -1
  32. data/test/site/layouts/tag_index.html +3 -2
  33. data/test/site/layouts/tag_page.html +6 -6
  34. data/test/site/options.yaml +2 -2
  35. data/test/site/plugins/test.rb +1 -3
  36. data/test/site/posts/Testing-Stuff.markdown +1 -1
  37. data/test/site/posts/Textile-Test.textile +1 -1
  38. data/test/site/posts/lorem-ipsum.markdown +1 -1
  39. data/test/site/posts/same-date.markdown +1 -1
  40. data/test/{test_henshin.rb → suite.rb} +0 -1
  41. data/test/test_gen.rb +98 -0
  42. data/test/test_options.rb +34 -19
  43. data/test/test_post.rb +67 -0
  44. data/test/test_site.rb +159 -46
  45. data/test/test_static.rb +13 -0
  46. metadata +53 -32
  47. data/lib/henshin/categories.rb +0 -29
  48. data/lib/henshin/tags.rb +0 -28
  49. data/test/test_archives.rb +0 -27
  50. data/test/test_categories.rb +0 -0
  51. data/test/test_gens.rb +0 -54
  52. data/test/test_layouts.rb +0 -14
  53. data/test/test_posts.rb +0 -75
  54. data/test/test_statics.rb +0 -0
  55. data/test/test_tags.rb +0 -0
  56. data/test/text_exts.rb +0 -0
@@ -1,63 +1,100 @@
1
1
  module Henshin
2
- class StandardPlugin
3
- # the main class for plugins to inherit from eg
2
+
3
+ class Plugin
4
+
5
+ # @return [Hash{:input, :output => Array, String}]
6
+ # the file extensions that can be read by the plugin and the extension
7
+ # of the output
4
8
  #
5
- # class MyPlugin < NoName::StandardPlugin
6
- #
7
- # or it can (and should) inherit a subclass from below
9
+ # @example
10
+ #
11
+ # @extensions = {:input => ['md', 'markdown'],
12
+ # :output => 'html'}
13
+ attr_accessor :extensions
8
14
 
9
- attr_accessor :extensions, :config
15
+ # @return [Hash{Symbol => Object}]
16
+ # the config for the plugin
17
+ attr_accessor :config
10
18
 
11
- # Defaults = {}
19
+ # @return [Integer]
20
+ # The plugins are sorted on priority, high priority plugins are called first.
21
+ # You could really use any number, but stick to 1 to 5.
22
+ attr_accessor :priority
12
23
 
13
- def initialize
14
- # inputs are the file types it will take
15
- # output should be the type it creates
24
+ # Create a new instance of Plugin
25
+ #
26
+ # @param [Site] site that the plugin belongs to
27
+ def initialize(site)
16
28
  @extensions = {:input => [],
17
29
  :output => ''}
30
+ @config = {}
31
+ @priority = 3
18
32
  end
19
33
 
20
- def configure( override )
21
- if Defaults
22
- if override
23
- @config = Defaults.merge(override)
34
+ # Finds all classes that subclass this particular class
35
+ #
36
+ # @return [Array] an array of class objects
37
+ # @see http://www.ruby-forum.com/topic/163430
38
+ # modified from the answer given on ruby-forum by black eyes
39
+ def self.subclasses
40
+ r = Henshin.constants.find_all do |c_klass|
41
+ if (c_klass != c_klass.upcase) && (Henshin.const_get(c_klass).is_a?(Class))
42
+ self > Henshin.const_get(c_klass)
24
43
  else
25
- @config = Defaults
44
+ nil
26
45
  end
27
- elsif override
28
- @config = override
29
46
  end
47
+ r.collect {|k| Henshin.const_get(k)}
30
48
  end
31
49
 
50
+ # Plugins are sorted by priority
32
51
  def <=>(other)
33
52
  self.priority <=> other.priority
34
53
  end
35
54
 
36
- # Uncomment to have the plugin loaded
37
- # Henshin.register! self, :standard_plugin
38
- end
55
+ end
39
56
 
40
- class Generator < StandardPlugin
41
- # a plugin which returns anything*
57
+ # Generator is the plugin type for processing things like markdown
58
+ #
59
+ # @example
60
+ #
61
+ # class MyMarkupPlugin < Henshin::Generator
62
+ # def generate(content)
63
+ # MyMarkup.do_stuff(content)
64
+ # end
65
+ # end
66
+ #
67
+ class Generator < Plugin
42
68
 
69
+ # This is the method that is called when rendering content
70
+ #
71
+ # @param [String] content to be rendered
72
+ # @return [String]
43
73
  def generate( content )
44
- # return string
45
74
  end
46
-
47
- # Uncomment to have the plugin loaded
48
- # Henshin.register! self, :generator
75
+
49
76
  end
50
77
 
51
- class LayoutParser < StandardPlugin
52
- # a plugin which returns anything*
78
+ # Layoutor is the plugin type for things like liquid
79
+ #
80
+ # @example
81
+ #
82
+ # class MyLayoutPlugin < Henshin::Layoutor
83
+ # def generate(content, data)
84
+ # MyLayout.do_stuff(content).render(data)
85
+ # end
86
+ # end
87
+ #
88
+ class Layoutor < Plugin
53
89
 
54
- # given a layout and data to insert
55
- def generate( layout, data )
56
- # return string
90
+ # This is the method called when rendering content
91
+ #
92
+ # @param [String] content to be rendered
93
+ # @param [Hash] data to be put into the content
94
+ # @return [String]
95
+ def generate( content, data )
57
96
  end
58
97
 
59
- # Uncomment to have the plugin loaded
60
- # Henshin.register! self, :generator
61
98
  end
62
99
 
63
100
  end
@@ -1,31 +1,24 @@
1
- require 'henshin/plugin'
2
1
  require 'simplabs/highlight'
3
2
 
4
- class HighlightPlugin < Henshin::Generator
5
-
6
- attr_accessor :priority, :config, :extensions
7
-
8
- def initialize
9
- @extensions = {:input => ['*']}
10
- @config = {}
11
- @priority = 1
12
- end
13
-
14
- def configure( override )
15
- @config.merge!(override) if override
16
- end
17
-
18
- def generate( content )
19
- content =~ /(\$highlight)\s+(.+)((\n.*)+)(\$end)/
20
- if $1
21
- lang = $2.to_sym
22
- code = $3[1..-1] # removes first new line
23
- insert = '<pre><code>' + Simplabs::Highlight.highlight(lang, code) + '</code></pre>'
24
- content.gsub(/(\$highlight.*\$end)/m, insert)
25
- else
26
- content
3
+ module Henshin
4
+ class HighlightPlugin < Generator
5
+
6
+ def initialize(site)
7
+ @extensions = {:input => ['*']}
8
+ @config = {}
9
+ @priority = 1
10
+ end
11
+
12
+ def generate( content )
13
+ content =~ /(\$highlight)\s+(.+)((\n.*)+)(\$end)/
14
+ if $1
15
+ lang = $2.to_sym
16
+ code = $3[1..-1] # removes first new line
17
+ insert = '<pre><code>' + Simplabs::Highlight.highlight(lang, code) + '</code></pre>'
18
+ content.gsub(/(\$highlight.*\$end)/m, insert)
19
+ else
20
+ content
21
+ end
27
22
  end
28
23
  end
29
-
30
- Henshin.register! self
31
24
  end
@@ -1,63 +1,61 @@
1
- require 'henshin/plugin'
2
1
  require 'liquid'
3
2
 
4
- class LiquidPlugin < Henshin::LayoutParser
5
-
6
- attr_accessor :config
7
-
8
- def initialize
9
- @config = {}
10
- end
11
-
12
- def configure( override )
13
- @config.merge!(override) if override
14
- end
15
-
16
- def generate( layout, data )
17
- reg = {:include_dir => @config['include_dir']}
18
- Liquid::Template.parse(layout).render(data, :registers => reg)
19
- end
20
-
21
- module Filters
22
- def date_to_string(dt)
23
- dt.strftime "%d %b %Y"
24
- end
3
+ module Henshin
4
+ class LiquidPlugin < Layoutor
25
5
 
26
- def date_to_long(dt)
27
- dt.strftime "%d %B %Y at %H:%M"
28
- end
29
-
30
- def time_to_string(dt)
31
- dt.strtime "%H:%M"
6
+ def initialize(site)
7
+ @config = {}
8
+
9
+ if site.config['liquid']
10
+ @config = site.config['liquid']
11
+ @config['include_dir'] = File.join(site.root, @config['include_dir'])
12
+ end
32
13
  end
33
14
 
34
- def titlecase(str)
35
- str.upcase
15
+ def generate( content, data )
16
+ reg = {:include_dir => @config['include_dir']}
17
+ Liquid::Template.parse(content).render(data, :registers => reg)
36
18
  end
37
19
 
38
- def escape(str)
39
- CGI::escape str
40
- end
20
+ module Filters
21
+ def date_to_string(dt)
22
+ dt.strftime "%d %b %Y"
23
+ end
24
+
25
+ def date_to_long(dt)
26
+ dt.strftime "%d %B %Y at %H:%M"
27
+ end
41
28
 
42
- def escape_html(str)
43
- CGI::escapeHTML str
44
- end
45
- end
46
- Liquid::Template.register_filter(Filters)
47
-
48
- class Include < Liquid::Tag
49
- def initialize(tag_name, file, tokens)
50
- super
51
- @file = file.strip
52
- end
29
+ def time_to_string(dt)
30
+ dt.strtime "%H:%M"
31
+ end
32
+
33
+ def titlecase(str)
34
+ str.upcase
35
+ end
36
+
37
+ def escape(str)
38
+ CGI::escape str
39
+ end
40
+
41
+ def escape_html(str)
42
+ CGI::escapeHTML str
43
+ end
44
+ end
45
+ Liquid::Template.register_filter(Filters)
46
+
47
+ class Include < Liquid::Tag
48
+ def initialize(tag_name, file, tokens)
49
+ super
50
+ @file = file.strip
51
+ end
52
+
53
+ def render(context)
54
+ include = File.join(context.registers[:include_dir], @file)
55
+ File.open(include, 'r') {|f| f.read}
56
+ end
57
+ end
58
+ Liquid::Template.register_tag('include', Include)
53
59
 
54
- def render(context)
55
- include = File.join(context.registers[:include_dir], @file)
56
- File.open(include, 'r') {|f| f.read}
57
- end
58
60
  end
59
- Liquid::Template.register_tag('include', Include)
60
-
61
- Henshin.register! self, :liquid
62
61
  end
63
-
@@ -1,20 +1,18 @@
1
- require 'henshin/plugin'
2
1
  require 'maruku'
3
2
 
4
- class MarukuPlugin < Henshin::Generator
5
-
6
- attr_accessor :extensions, :config, :priority
7
-
8
- def initialize( override={} )
9
- @extensions = {:input => ['markdown', 'mkdwn', 'md'],
10
- :output => 'html'}
11
- @config = {}
12
- @priority = 5
3
+ module Henshin
4
+ class MarukuPlugin < Generator
5
+
6
+ def initialize(site)
7
+ @extensions = {:input => ['markdown', 'mkdwn', 'md'],
8
+ :output => 'html'}
9
+ @config = {}
10
+ @priority = 5
11
+ end
12
+
13
+ def generate( content )
14
+ Maruku.new(content).to_html
15
+ end
16
+
13
17
  end
14
-
15
- def generate( content )
16
- Maruku.new(content).to_html
17
- end
18
-
19
- Henshin.register! self
20
18
  end
@@ -1,28 +1,24 @@
1
- require 'henshin/plugin'
2
1
  require 'sass'
3
2
 
4
- class SassPlugin < Henshin::Generator
3
+ module Henshin
4
+ class SassPlugin < Generator
5
+
6
+ def initialize(site)
7
+ @extensions = {:input => ['sass', 'scss'],
8
+ :output => 'css'}
9
+ @config = {'ignore_layouts' => true,
10
+ 'style' => :nested,
11
+ 'load_paths' => Dir.glob((site.root + '*').to_s),
12
+ 'syntax' => :sass}
5
13
 
6
- attr_accessor :extensions, :config, :priority
7
-
8
- Defaults = {:ignore_layouts => true,
9
- :style => :nested}
10
-
11
- def initialize
12
- @extensions = {:input => ['sass', 'scss'],
13
- :output => 'css'}
14
- @config = {:ignore_layouts => true,
15
- :style => :nested}
16
- @priority = 5
17
- end
18
-
19
- def configure( override )
20
- @config.merge!(override) if override
21
- end
22
-
23
- def generate( content )
24
- Sass::Engine.new(content, @config).render
14
+ @config.merge!(site.config['sass']) if site.config['sass']
15
+
16
+ @priority = 5
17
+ end
18
+
19
+ def generate( content )
20
+ Sass::Engine.new(content, @config.to_options).render
21
+ end
22
+
25
23
  end
26
-
27
- Henshin.register! self, :sass
28
24
  end
@@ -1,20 +1,18 @@
1
- require 'henshin/plugin'
2
1
  require 'redcloth'
3
2
 
4
- class TextilePlugin < Henshin::Generator
5
-
6
- attr_accessor :extensions, :config, :priority
7
-
8
- def initialize( override={} )
9
- @extensions = {:input => ['textile'],
10
- :output => 'html'}
11
- @config = {}
12
- @priority = 5
13
- end
14
-
15
- def generate( content )
16
- RedCloth.new(content).to_html
3
+ module Henshin
4
+ class TextilePlugin < Generator
5
+
6
+ def initialize(site)
7
+ @extensions = {:input => ['textile'],
8
+ :output => 'html'}
9
+ @config = {}
10
+ @priority = 5
11
+ end
12
+
13
+ def generate( content )
14
+ RedCloth.new(content).to_html
15
+ end
16
+
17
17
  end
18
-
19
- Henshin.register! self
20
18
  end
@@ -2,112 +2,55 @@ module Henshin
2
2
 
3
3
  class Post < Gen
4
4
 
5
- attr_accessor :title, :author, :tags, :category, :date
5
+ attr_accessor :path, :data, :content, :site, :layout, :generators
6
6
 
7
7
  def initialize( path, site )
8
8
  @path = path
9
9
  @site = site
10
- @config = site.config
11
- @extension = path.extension
12
- @layout = site.layouts[ site.config[:layout] ]
13
- @author = @config[:author]
14
- @tags = []
15
- @date = Time.now
10
+
11
+ @content = ''
12
+ @data = {}
13
+ @generators = []
14
+
15
+ @data['input'] = @path.extname[1..-1]
16
16
  end
17
17
 
18
18
 
19
19
  ##
20
- # Processes the file
21
- def process
20
+ # Reads the file
21
+ def read
22
22
  self.read_name
23
- self.read_yaml
23
+ self.read_file if @path.exist?
24
+ self.get_generators
25
+ self.get_layout
26
+
27
+ # now tidy up data
28
+ @data['output'] ||= @data['input']
29
+ @data['date'] = Time.parse(@data['date'])
30
+ @data['tags'] = @data['tags'].flatten.uniq if @data['tags']
31
+ self
24
32
  end
25
33
 
26
34
  # Reads the filename and extracts information from it
27
35
  def read_name
28
-
29
- parser = {'title' => '([a-zA-Z0-9 ]+)',
30
- 'title-with-dashes' => '([a-zA-Z0-9-]+)',
31
- 'date' => '(\d{4}-\d{2}-\d{2})',
32
- 'date-time' => '(\d{4}-\d{2}-\d{2} at \d{2}:\d{2})',
33
- 'xml-date-time' => '(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?((\+|-)\d{2}:\d{2})?)',
34
- 'category' => '([a-zA-Z0-9_ -]+)',
35
- 'extension' => "([a-zA-Z0-9_-]+)"}
36
36
 
37
- file_parser = config[:file_name]
38
- data_order = []
37
+ result = Parsey.parse(@path.to_s[(@site.root + 'posts').to_s.size..-1], @site.config['file_name'], Partials)
39
38
 
40
- # put together regex
41
- m = file_parser.gsub(/\{([a-z-]+)\}/) do
42
- data_order << $1
43
- parser[$1]
44
- end
45
-
46
- # replace optional '<stuff>'
47
- m.gsub!(/<(.+)>/) do
48
- # this may lead to problems, well I say may...
49
- data_order.unshift( 'optional' )
50
- "(#{$1})?"
51
- end
52
-
53
- # convert string to actual regex
54
- matcher = Regexp.new(m)
55
-
56
- override = {}
57
- name = @path[ (config[:root]+'/posts/').size..-1 ]
58
-
59
- file_data = name.match( matcher ).captures
60
- file_data.each_with_index do |data, i|
61
- if data_order[i].include? 'title'
62
- if data_order[i].include? 'dashes'
63
- override[:title] = data.gsub(/-/, ' ')
64
- else
65
- override[:title] = data
39
+ result.each do |k, v|
40
+ unless v.nil?
41
+ case k
42
+ when 'title-with-dashes'
43
+ @data['title'] = v.gsub(/-/, ' ').titlecase
44
+ when 'title'
45
+ @data['title'] = v.titlecase
46
+ else
47
+ @data[k] = v
66
48
  end
67
- elsif data_order[i].include? 'date'
68
- override[:date] = data
69
- elsif data_order[i].include? 'extension'
70
- override[:extension] = data
71
- elsif data_order[i].include? 'category'
72
- override[:category] = data
73
49
  end
74
50
  end
75
- self.override( override )
76
- end
77
-
78
- # Reads the files yaml frontmatter and uses it to override some settings, then grabs content
79
- def read_yaml
80
- file = File.read(self.path)
81
-
82
- if file =~ /^(---\s*\n.*?\n?^---\s*$\n?)/m
83
- override = YAML.load_file(@path).to_options
84
- self.override(override)
85
- @content = file[$1.size..-1]
86
- else
87
- @content = file
88
- end
89
- end
90
-
91
- # Uses the loaded data to override settings
92
- #
93
- # @param [Hash] override data to override settings with
94
- def override( override )
95
- @title = override[:title].titlecase if override[:title]
96
- @layout = @site.layouts[ override[:layout] ] if override[:layout]
97
- @date = Time.parse( override[:date] ) if override[:date]
98
- @tags = override[:tags].split(', ') if override[:tags]
99
- @category = override[:category] if override[:category]
100
- @author = override[:author] if override[:author]
101
- @extension = override[:extension] if override[:extension]
102
- @category = override[:category] if override[:category]
103
51
 
104
- if override[:tags]
105
- @tags << override[:tags].split(', ')
106
- @tags.flatten!.uniq!
107
- end
108
52
  end
109
53
 
110
-
111
54
  # Creates the data to be sent to the layout engine
112
55
  #
113
56
  # @return [Hash] the payload for the layout engine
@@ -117,8 +60,8 @@ module Henshin
117
60
  'site' => @site.payload['site'],
118
61
  'post' => self.to_hash
119
62
  }
120
- r['post']['next'] = self.next.to_hash if self.next
121
- r['post']['prev'] = self.prev.to_hash if self.prev
63
+ #r['post']['next'] = self.next.to_hash if self.next
64
+ #r['post']['prev'] = self.prev.to_hash if self.prev
122
65
  r
123
66
  end
124
67
 
@@ -126,20 +69,34 @@ module Henshin
126
69
  #
127
70
  # @return [Hash]
128
71
  def to_hash
129
- tags = []
130
- @site.tags.select{ |t| @tags.include?(t) }.each do |k, tag|
131
- tags << {'name' => tag.name, 'url' => tag.url}
72
+ if @hashed
73
+ @hashed
74
+ else
75
+ @hashed = @data.dup
76
+ @hashed['content'] = @content
77
+ @hashed['url'] = self.url
78
+ @hashed['permalink'] = self.permalink
79
+
80
+
81
+
82
+ if @data['tags']
83
+ @hashed['tags'] = []
84
+ @site.tags.select{|t| @data['tags'].include?(t.name)}.each do |tag|
85
+ # can't call Tag#to_hash or it creates an infinite loop!
86
+ @hashed['tags'] << {'name' => tag.name, 'url' => tag.url}
87
+ end
88
+ end
89
+
90
+ if @data['category']
91
+ @site.categories.each do |cat|
92
+ if cat.name == @data['category']
93
+ @hashed['category'] = {'name' => cat.name, 'url' => cat.url}
94
+ end
95
+ end
96
+ end
97
+
98
+ @hashed
132
99
  end
133
- {
134
- 'title' => @title,
135
- 'author' => @author,
136
- 'permalink' => self.permalink,
137
- 'url' => self.url,
138
- 'date' => @date,
139
- 'category' => @category,
140
- 'tags' => tags,
141
- 'content' => @content
142
- }
143
100
  end
144
101
 
145
102
  # Gets the post after this one
@@ -167,32 +124,22 @@ module Henshin
167
124
  end
168
125
  end
169
126
  end
170
-
171
-
172
- ##
173
- # Writes the file to the correct place
174
- def write
175
- write_path = File.join( config[:root], config[:target], permalink )
176
- FileUtils.mkdir_p write_path.directory
177
- file = File.new( write_path, "w" )
178
- file.puts( @content )
179
- end
180
127
 
181
- # Creates the permalink for the post
128
+ # @return [String] the permalink of the post
182
129
  def permalink
183
- partials = {'year' => self.date.year,
184
- 'month' => self.date.month,
185
- 'date' => self.date.day,
186
- 'title' => self.title.slugify,
187
- 'category' => self.category || ''}
130
+ partials = {'year' => @data['date'].year,
131
+ 'month' => @data['date'].month,
132
+ 'date' => @data['date'].day,
133
+ 'title' => @data['title'].slugify,
134
+ 'category' => @data['category'] || ''}
188
135
 
189
- config[:permalink].gsub(/\{([a-z-]+)\}/) do
190
- partials[$1]
191
- end
136
+ perm = @site.config['permalink'].gsub(/\{([a-z-]+)\}/) { partials[$1] }
137
+ File.join(@site.base, perm)
192
138
  end
193
139
 
140
+ # Sorts on date first, then permalink if dates are equal
194
141
  def <=>(other)
195
- s = self.date <=> other.date
142
+ s = @data['date'] <=> other.data['date']
196
143
  if s == 0
197
144
  self.permalink <=> other.permalink
198
145
  else