zzot-zzot-semi-static 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/History.txt +6 -0
  2. data/Manifest.txt +30 -0
  3. data/README.markdown +84 -0
  4. data/VERSION.yml +4 -0
  5. data/bin/semi +6 -0
  6. data/lib/semi-static.rb +30 -0
  7. data/lib/semi-static/base.rb +74 -0
  8. data/lib/semi-static/categories.rb +74 -0
  9. data/lib/semi-static/cli.rb +126 -0
  10. data/lib/semi-static/convertable.rb +94 -0
  11. data/lib/semi-static/core_ext/hash.rb +19 -0
  12. data/lib/semi-static/index.rb +12 -0
  13. data/lib/semi-static/layout.rb +14 -0
  14. data/lib/semi-static/page.rb +58 -0
  15. data/lib/semi-static/post.rb +133 -0
  16. data/lib/semi-static/posts.rb +110 -0
  17. data/lib/semi-static/pygmentize.rb +54 -0
  18. data/lib/semi-static/site.rb +232 -0
  19. data/lib/semi-static/snippet.rb +12 -0
  20. data/lib/semi-static/statistics.rb +45 -0
  21. data/lib/semi-static/stylesheet.rb +33 -0
  22. data/test/helper.rb +111 -0
  23. data/test/ref/test_layout/default_layout.html +35 -0
  24. data/test/ref/test_layout/post_layout.html +85 -0
  25. data/test/ref/test_output/2005-03-27.html +5 -0
  26. data/test/ref/test_output/2005-03.html +4 -0
  27. data/test/ref/test_output/2005.html +5 -0
  28. data/test/ref/test_output/2008-11-24.html +5 -0
  29. data/test/ref/test_output/2008-11-26.html +5 -0
  30. data/test/ref/test_output/2008-11.html +5 -0
  31. data/test/ref/test_output/2008-12-04.html +5 -0
  32. data/test/ref/test_output/2008-12.html +4 -0
  33. data/test/ref/test_output/2008.html +6 -0
  34. data/test/ref/test_page/about.html +47 -0
  35. data/test/ref/test_page/colophon.html +45 -0
  36. data/test/ref/test_post/impressions.html +102 -0
  37. data/test/ref/test_post/lighting-up.html +102 -0
  38. data/test/ref/test_post/the-working-mans-typeface.html +88 -0
  39. data/test/source/indices/day.erb +7 -0
  40. data/test/source/indices/month.erb +10 -0
  41. data/test/source/indices/year.erb +10 -0
  42. data/test/source/layouts/default.haml +25 -0
  43. data/test/source/layouts/post.erb +36 -0
  44. data/test/source/pages/about.md +38 -0
  45. data/test/source/pages/colophon.md +36 -0
  46. data/test/source/pages/feed.xml.erb +26 -0
  47. data/test/source/posts/2005-03-27-a-bash-script-to-mess-with-the-containing-terminalapp-window.markdown +38 -0
  48. data/test/source/posts/2008-11-24-lighting-up.markdown +41 -0
  49. data/test/source/posts/2008-11-26-impressions.md +42 -0
  50. data/test/source/posts/2008-12-04-the-working-mans-typeface.html +15 -0
  51. data/test/source/scripts/jquery-1.3.js +4241 -0
  52. data/test/source/scripts/jquery-1.3.min.js +19 -0
  53. data/test/source/semi.yml +5 -0
  54. data/test/source/snippets/comment-links.html +17 -0
  55. data/test/source/snippets/comments.html +4 -0
  56. data/test/source/stylesheets/layout.sass +115 -0
  57. data/test/source/stylesheets/post.sass +21 -0
  58. data/test/source/stylesheets/screen.sass +11 -0
  59. data/test/source/stylesheets/syntax.css +60 -0
  60. data/test/source/stylesheets/text.sass +25 -0
  61. data/test/test_layout.rb +51 -0
  62. data/test/test_output.rb +61 -0
  63. data/test/test_page.rb +68 -0
  64. data/test/test_post.rb +96 -0
  65. metadata +183 -0
@@ -0,0 +1,19 @@
1
+ module SemiStatic
2
+ module CoreExt #:nodoc:
3
+ module Hash
4
+ # Replace self with a hash whose keys have been converted to symbols.
5
+ def symbolize_keys
6
+ hash = to_a.inject({}) do |memo,item|
7
+ key, value = item
8
+ memo[key.to_sym] = value
9
+ memo
10
+ end
11
+ replace(hash)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ class Hash #:nodoc:
18
+ include SemiStatic::CoreExt::Hash
19
+ end
@@ -0,0 +1,12 @@
1
+ module SemiStatic
2
+ class Index < Base
3
+ include Convertable
4
+
5
+ attr_accessor :posts, :context
6
+
7
+ def initialize(site, path)
8
+ super
9
+ @metadata = [ :layout ]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module SemiStatic
2
+ class Layout < Base
3
+ include Convertable
4
+
5
+ attr_reader :name
6
+
7
+ def initialize(site, path)
8
+ super
9
+ @metadata = [ :layout ]
10
+
11
+ @name = File.basename(source_path, source_ext)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ module SemiStatic
2
+ # Page represents a static page in the site. Page itself only
3
+ # represents a single source file, but can have a Layout and any number
4
+ # of Snippets, each of which has its own source file. Page's modification
5
+ # time is the most recent of itself, its layout, and any Snippets used.
6
+ class Page < Base
7
+ include Convertable
8
+
9
+ ##
10
+ # The Page's output directory
11
+ attr_reader :output_dir
12
+
13
+ ##
14
+ # The Page's output path
15
+ attr_reader :output_path
16
+
17
+ ##
18
+ # The Page's name
19
+ attr_reader :name
20
+
21
+ ##
22
+ # The Page's URI
23
+ attr_reader :uri
24
+
25
+ ##
26
+ # Initializes a new Page
27
+ #
28
+ # +site+:: The Site object we belong to
29
+ # +path+:: The relative path to the source file
30
+ def initialize(site, path)
31
+ super
32
+ @metadata = [ :title, :layout ]
33
+
34
+ src_base = File.basename(source_path, source_ext)
35
+ output_ext = File.extname(src_base)
36
+ if output_ext.nil? || output_ext.empty?
37
+ output_ext = '.html'
38
+ else
39
+ src_base = File.basename(src_base, output_ext)
40
+ end
41
+ @output_dir, src_file = File.split(source_path)
42
+
43
+ if output_dir == '.'
44
+ prefix = ''
45
+ else
46
+ prefix = "#{output_dir}/"
47
+ end
48
+ @name = "/#{prefix}#{src_base}"
49
+ @output_path = "#{prefix}#{src_base}#{output_ext}"
50
+
51
+ if src_base == 'index'
52
+ @uri = "/#{output_dir}/"
53
+ else
54
+ @uri = "/#{output_path}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,133 @@
1
+ module SemiStatic
2
+ # Post represents a single post in the site. Post itself only
3
+ # represents a single source file, but can have a Layout and any number
4
+ # of Snippets, each of which has its own source file. Post's modification
5
+ # time is the most recent of itself, its Layout, and any Snippets used.
6
+ class Post < Base
7
+ include Convertable
8
+
9
+ ##
10
+ # The Post's name
11
+ attr_reader :name
12
+
13
+ ##
14
+ # The Post's Category
15
+ attr_reader :category
16
+
17
+ ##
18
+ # The Post's Tags
19
+ attr_reader :tags
20
+
21
+ ##
22
+ # The Post's output directory
23
+ attr_reader :output_dir
24
+
25
+ ##
26
+ # The Post's output path
27
+ attr_reader :output_path
28
+
29
+ ##
30
+ # The Post's slug
31
+ attr_reader :slug
32
+
33
+ ##
34
+ # The Post's URI
35
+ attr_reader :uri
36
+
37
+ ##
38
+ # Date Post was created
39
+ attr_reader :created
40
+
41
+ ##
42
+ # Date the Post was last updated
43
+ attr_reader :updated
44
+
45
+ # attr_reader :year
46
+ # attr_reader :month
47
+ # attr_reader :day
48
+
49
+ ##
50
+ # Initializes a new Post
51
+ #
52
+ # +site+:: The Site object we belong to
53
+ # +path+:: The relative path to the source file
54
+ def initialize(site, path)
55
+ super
56
+ @metadata = [ :title, :layout, :author ]
57
+
58
+ @name = File.basename(source_path, source_ext)
59
+
60
+ @category = site.categories[source_metadata[:category]]
61
+ @category << self
62
+
63
+ @tags = []
64
+ unless source_metadata[:tags].nil?
65
+ for tag in source_metadata[:tags]
66
+ @tags << site.tags[tag]
67
+ @tags.last << self
68
+ end
69
+ end
70
+
71
+ if name =~ /^(\d+-\d+-\d+)-(.+)$/
72
+ @created = Time.parse $1
73
+ @updated ||= @created
74
+ @slug = $2
75
+ @output_dir = created.strftime('%Y/%m/%d')
76
+ @output_path = File.join output_dir, "#{slug}.html"
77
+ @uri = "/#{output_path}"
78
+ else
79
+ raise ArgumentError, "Bad file name: #{name}"
80
+ end
81
+ end
82
+
83
+ ##
84
+ # Get the Post's unique identifier
85
+ def id
86
+ name.gsub /-/, '_'
87
+ end
88
+
89
+ ##
90
+ # Get the Post's permalink
91
+ def permalink
92
+ return "#{uri}"
93
+ end
94
+
95
+ # def comments_link
96
+ # "#{uri}#comments"
97
+ # end
98
+ #
99
+ # def comment_count
100
+ # 0
101
+ # end
102
+
103
+ ##
104
+ # Formatted date the Post was published (i.e., "March 15, 2009")
105
+ def date
106
+ created.strftime '%B %e, %Y'
107
+ end
108
+
109
+ ##
110
+ # Year the Post was published as a String (i.e., "2009")
111
+ def year
112
+ created.strftime '%Y'
113
+ end
114
+
115
+ ##
116
+ # Month the Post was published as a String (i.e., "03")
117
+ def month
118
+ created.strftime '%m'
119
+ end
120
+
121
+ ##
122
+ # Day the Post was published as a String (i.e., "15")
123
+ def day
124
+ created.strftime '%d'
125
+ end
126
+
127
+ ##
128
+ # Does the post have a Time set?
129
+ def time?
130
+ false
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,110 @@
1
+ module SemiStatic
2
+ class Posts
3
+ attr_reader :site, :posts, :names, :indices
4
+
5
+ def initialize(site)
6
+ @site = site
7
+ @posts = []
8
+ @names = {}
9
+ @indices = Set.new
10
+ end
11
+
12
+ NAME_RE = /^([0-9]{4})-([0-9]{2})-([0-9]{2})-(.*)/
13
+
14
+ def <<(source_path)
15
+ unless File.file?(source_path)
16
+ $stderr.puts "[#{source_path}]"
17
+ return
18
+ end
19
+ if source_path =~ NAME_RE
20
+ post = Post.new site, source_path
21
+ self.posts.push post
22
+ names[post.name] = post
23
+ indices << File.join(post.year, post.month, post.day)
24
+ indices << File.join(post.year, post.month)
25
+ indices << post.year
26
+ else
27
+ $stderr.puts "{#{source_path}}"
28
+ end
29
+ end
30
+
31
+ def chop!(count)
32
+ raise ArgumentError unless count > 0
33
+ posts = self.posts.first(count)
34
+ @posts = []
35
+ @names = {}
36
+ @indices = Set.new
37
+ for post in posts
38
+ self.posts << post
39
+ names[post.name] = post
40
+ indices << File.join(post.year, post.month, post.day)
41
+ indices << File.join(post.year, post.month)
42
+ indices << post.year
43
+ end
44
+ end
45
+
46
+ def length
47
+ self.posts.length
48
+ end
49
+
50
+ def first(n=nil)
51
+ if n.nil?
52
+ self.posts.first
53
+ else
54
+ self.posts.first(n)
55
+ end
56
+ end
57
+
58
+ def last(n=nil)
59
+ if n.nil?
60
+ self.posts.last
61
+ else
62
+ self.posts.last(n).reverse
63
+ end
64
+ end
65
+
66
+ def from(year, month=nil, day=nil)
67
+ if year.is_a?(String) && month.nil? && day.nil?
68
+ date = year.split('/', 3)
69
+ year, month, day = date.collect { |c| c.to_i }
70
+ end
71
+
72
+ if month.nil?
73
+ from = Time.local year
74
+ to = Time.local(year + 1) - 1
75
+ elsif day.nil?
76
+ from = Time.local year, month
77
+ if month == 12
78
+ to = Time.local(year + 1, 1) - 1
79
+ else
80
+ to = Time.local(year, month + 1) - 1
81
+ end
82
+ else
83
+ from = Time.local year, month, day
84
+ to = Time.local year, month, day, 23, 59, 59
85
+ end
86
+ range = from..to
87
+ result = self.posts.select { |p| range.include?(p.created) }
88
+ class << result
89
+ alias_method :last_without_reverse, :last
90
+ def last_with_reverse(n=nil)
91
+ last_without_reverse(n).reverse
92
+ end
93
+ alias_method :last, :last_with_reverse
94
+ end
95
+ return result
96
+ end
97
+
98
+ def [](name)
99
+ return self.names[name]
100
+ end
101
+
102
+ def each(&block)
103
+ self.posts.each(&block)
104
+ end
105
+
106
+ def each_index(&block)
107
+ indices.each(&block)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,54 @@
1
+ module SemiStatic
2
+ module Pygmentize
3
+ LEXER_FORMAT = /^[a-z]+$/i
4
+
5
+ def pygmentize(code, lang)
6
+ Pygmentize.pygmentize code, lang
7
+ end
8
+
9
+ def self.pygmentize(code, lang)
10
+ unless lang =~ LEXER_FORMAT
11
+ raise ArgumentError, "invalid lexer: #{lang}"
12
+ end
13
+
14
+ Tempfile.open('semistatic-pygmentize') do |temp|
15
+ temp.write code
16
+ temp.close
17
+
18
+ cmd = "pygmentize -f html -l #{lang} #{temp.path}"
19
+ IO.popen(cmd) do |proc|
20
+ return proc.read
21
+ end
22
+ end
23
+ end
24
+
25
+ @@enabled = false
26
+ def self.enabled
27
+ @@enabled
28
+ end
29
+ def self.enabled=(value)
30
+ @@enabled = value
31
+ end
32
+ end
33
+ end
34
+
35
+ module MaRuKu #:nodoc:
36
+ module Out #:nodoc:
37
+ module HTML #:nodoc:
38
+ alias_method :to_html_code_without_pygments, :to_html_code
39
+ def to_html_code_with_pygments
40
+ if SemiStatic::Pygmentize.enabled
41
+ source = self.raw_code
42
+ lang = self.attributes[:lang] || 'text'
43
+ html = SemiStatic::Pygmentize.pygmentize source, lang
44
+ doc = Document.new html, :respect_whitespace => :all
45
+
46
+ add_ws doc.root
47
+ else
48
+ to_html_code_without_pygments
49
+ end
50
+ end
51
+ alias_method :to_html_code, :to_html_code_with_pygments
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,232 @@
1
+ module SemiStatic
2
+ class Site
3
+ attr_accessor :clean_first, :check_mtime, :quick_mode, :show_statistics
4
+
5
+ attr_reader :time
6
+ attr_reader :source_dir, :layouts, :pages, :posts, :snippets, :categories, :tags
7
+ attr_reader :year_index, :month_index, :day_index
8
+ attr_reader :metadata, :stylesheets
9
+
10
+ attr_reader :stats
11
+
12
+ def initialize(source_dir)
13
+ @clean_first = false
14
+ @first_pass = true
15
+ @quick_mode = false
16
+ @check_mtime = false
17
+ @show_statistics = false
18
+ @source_dir = source_dir
19
+ @time = Time.now
20
+ @stats = Statistics.new
21
+ stats.record(:site, :load) { load }
22
+ end
23
+
24
+ def self.open(source_dir)
25
+ raise ArugmentError, "block required" unless block_given?
26
+ site = SemiStatic::Site.new source_dir
27
+ yield site
28
+ end
29
+
30
+ def output(path)
31
+ if @first_pass
32
+ @first_pass = false
33
+ FileUtils.rm_rf path if clean_first
34
+ end
35
+ FileUtils.mkdir_p path
36
+
37
+ if quick_mode
38
+ posts.chop! 20
39
+ end
40
+ @stats.reset
41
+
42
+ unless metadata.nil? || !metadata['static'].is_a?(Array)
43
+ stats.record(:site, :static) do
44
+ for dir in metadata['static']
45
+ FileUtils.cp_r File.join(source_dir, dir), File.join(path, dir)
46
+ end
47
+ end
48
+ end
49
+
50
+ before = Time.now
51
+ Dir.chdir(path) do
52
+ stats.record(:site, :pages) do
53
+ pages.each do |name, page|
54
+ FileUtils.mkdir_p page.output_dir unless File.directory?(page.output_dir)
55
+ if check_mtime
56
+ if File.file?(page.output_path) && File.mtime(page.output_path) > page.source_mtime
57
+ next
58
+ end
59
+ page.load
60
+ end
61
+ File.open(page.output_path, 'w') { |f| f.write page.render }
62
+ end
63
+ end
64
+
65
+ stats.record(:site, :posts) do
66
+ posts.each do |post|
67
+ FileUtils.mkdir_p post.output_dir unless File.directory?(post.output_dir)
68
+ if check_mtime
69
+ if File.file?(post.output_path) && File.mtime(post.output_path) > post.source_mtime
70
+ next
71
+ end
72
+ post.load
73
+ end
74
+ File.open(post.output_path, 'w') { |f| f.write post.render }
75
+ end
76
+ end
77
+
78
+ stats.record(:site, :stylesheets) do
79
+ unless stylesheets.nil?
80
+ stylesheets.each do |name, stylesheet|
81
+ FileUtils.mkdir_p stylesheet.output_dir unless File.directory?(stylesheet.output_dir)
82
+ if check_mtime
83
+ if File.file?(stylesheet.output_path) && File.mtime(stylesheet.output_path) > stylesheet.source_mtime
84
+ next
85
+ end
86
+ stylesheet.load
87
+ end
88
+ File.open(stylesheet.output_path, 'w') { |f| f.write stylesheet.render }
89
+ end
90
+ end
91
+ end
92
+
93
+ stats.record(:site, :indices) do
94
+ unless year_index.nil? && month_index.nil? && day_index.nil?
95
+ posts.each_index do |dir|
96
+ posts = self.posts.from(dir)
97
+ Dir.chdir(dir) do
98
+ context = dir.split('/').collect { |c| c.to_i }
99
+ date_index = case context.length
100
+ when 1
101
+ year_index
102
+ when 2
103
+ month_index
104
+ when 3
105
+ day_index
106
+ else
107
+ nil
108
+ end
109
+ date_index.posts = posts
110
+ date_index.context = Time.local *context
111
+ File.open('index.html', 'w') { |f| f.write date_index.render }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ self.stats.display if show_statistics
119
+ end
120
+
121
+ private
122
+ def with_source_files(subdir, pattern)
123
+ raise ArgumentError unless block_given?
124
+ Dir.chdir(File.join(source_dir, subdir)) do
125
+ Dir.glob(pattern) do |path|
126
+ yield path
127
+ end
128
+ end
129
+ end
130
+
131
+ def config_file_path
132
+ File.join source_dir, 'semi.yml'
133
+ end
134
+
135
+ def load
136
+ if File.file?(config_file_path)
137
+ @metadata = File.open(config_file_path) { |io| YAML.load io }
138
+ end
139
+ load_stylesheets
140
+ load_layouts
141
+ load_pages
142
+ load_posts
143
+ load_snippets
144
+ load_indices
145
+ end
146
+
147
+ def load_stylesheets
148
+ unless metadata.nil? || metadata['stylesheets'].nil?
149
+ Dir.chdir(File.join(source_dir, 'stylesheets')) do
150
+ config = metadata['stylesheets']
151
+ if config.is_a?(Array)
152
+ config = config.inject({}) { |hash,name| hash[name] = {}; hash }
153
+ end
154
+ @stylesheets = config.inject({}) do |hash,pair|
155
+ name, opts = pair
156
+ hash[name.to_sym] = Stylesheet.new self, name, opts
157
+ hash
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ def load_layouts
164
+ @layouts = Hash.new
165
+ with_source_files('layouts', '*.{haml,erb}') do |path|
166
+ next unless File.file?(path)
167
+
168
+ file = File.basename(path)
169
+ if file[0..0] != '_'
170
+ layout = Layout.new self, path
171
+ self.layouts[layout.name.to_sym] = layout
172
+ end
173
+ end
174
+ end
175
+
176
+ def load_pages
177
+ @pages = Hash.new
178
+ with_source_files('pages', '**/*.{html,haml,erb,txt,md,markdown}') do |path|
179
+ next if File.directory?(path)
180
+ next unless path.split('/').grep(/^_/).empty?
181
+
182
+ page = Page.new self, path
183
+ self.pages[page.name] = page
184
+ end
185
+ end
186
+
187
+ def load_posts
188
+ @posts = Posts.new(self)
189
+ @categories = Categories.new
190
+ @tags = Categories.new
191
+ with_source_files('posts', '*.{html,haml,erb,txt,md,markdown}') do |path|
192
+ posts << path
193
+ end
194
+ posts.posts.sort! { |l,r| l.created <=> r.created }
195
+ end
196
+
197
+ def load_snippets
198
+ return unless File.directory?(File.join(source_dir, 'snippets'))
199
+
200
+ @snippets = Hash.new
201
+ with_source_files('snippets', '*.{html,haml,erb,txt,md,markdown}') do |path|
202
+ next unless File.file?(path)
203
+ next unless ('a'..'z').include?(path[0..0].downcase)
204
+
205
+ snippet = Snippet.new self, path
206
+ self.snippets[snippet.name] = snippet
207
+ end
208
+ end
209
+
210
+ def load_indices
211
+ return unless File.directory?(File.join(source_dir, 'indices'))
212
+
213
+ with_source_files('indices', '{year,month,day}.{haml,erb}') do |path|
214
+ # puts path
215
+ next unless File.file?(path)
216
+
217
+ file = File.basename(path)
218
+ name = File.basename(file, File.extname(file))
219
+ case name
220
+ when 'year'
221
+ @year_index = Index.new self, path
222
+ when 'month'
223
+ @month_index = Index.new self, path
224
+ when 'day'
225
+ @day_index = Index.new self, path
226
+ else
227
+ raise ArgumentError, "Unexpected index file: #{path}"
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end