henshin 0.3.0 → 0.4.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 (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