Class: Zenweb::Page

Inherits:
Object
  • Object
show all
Includes:
Rake::DSL
Defined in:
lib/zenweb/page.rb,
lib/zenweb/plugins/erb.rb,
lib/zenweb/plugins/less.rb,
lib/zenweb/plugins/disqus.rb,
lib/zenweb/plugins/google.rb,
lib/zenweb/plugins/markdown.rb

Overview

Page represents pretty much any type of file that goes on your website or is needed by other pages to build your website. Each page can have a YAML header that contains configuration data or variables used in the page.

Direct Known Subclasses

FakePage

Defined Under Namespace

Modules: MarkdownHelpers

Constant Summary collapse

KRAMDOWN_CONFIG =

:nodoc:

{ # :nodoc:
  :toc_levels    => '2..4',

  :entity_output => :symbolic,

  :coderay_wrap               => :div,
  :coderay_line_numbers       => :table,
  :coderay_tab_width          => 4,
  :coderay_css                => :class,
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(site, path, config = nil) ⇒ Page

:nodoc:



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/zenweb/page.rb', line 54

def initialize site, path, config = nil # :nodoc:
  # TODO: make sure that creating page /a.html strips leading / from path
  @site, @path = site, path
  @config = config if config
  @binary = config

  self.filetypes.each do |type|
    send "extend_#{type}" if self.respond_to? "extend_#{type}"
  end

  @subpages = []
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(msg, *args) ⇒ Object

Access a config variable and only warn if it isn’t accessible. If msg starts with render, go ahead and pass that up to the default method_missing.



338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/zenweb/page.rb', line 338

def method_missing msg, *args # :nodoc:
  case msg.to_s
  when /=|^render_|^to_a(?:ry)?$/ then # to_a/ry for 1.9 only. :(
    super
  else
    if config.key? msg
      config[msg]
    else
      config.send msg, *args
    end
  end
end

Instance Attribute Details

#binaryObject Also known as: binary?

Is this file a binary file? Defaults to true if config passed to Page.new.



37
38
39
# File 'lib/zenweb/page.rb', line 37

def binary
  @binary
end

#parentObject

The parent page of this page. Can be nil.



32
33
34
# File 'lib/zenweb/page.rb', line 32

def parent
  @parent
end

#pathObject (readonly)

The path to this source file.



22
23
24
# File 'lib/zenweb/page.rb', line 22

def path
  @path
end

#siteObject (readonly)

The shared site instance.



17
18
19
# File 'lib/zenweb/page.rb', line 17

def site
  @site
end

#subpagesObject (readonly)

The pages directly below this page. Can be empty.



27
28
29
# File 'lib/zenweb/page.rb', line 27

def subpages
  @subpages
end

Class Method Details

.renderers_reObject

Returns a regexp that will match file extensions for all known renderer types.



44
45
46
47
48
49
50
51
52
# File 'lib/zenweb/page.rb', line 44

def self.renderers_re
  @renderers_re ||=
    begin
      ext = instance_methods.grep(/^render_/).map { |s|
        s.to_s.sub(/render_/, '')
      }
      /(?:\.(#{ext.join "|"}))+$/
    end
end

Instance Method Details

#[](k) ⇒ Object

Helper method to access the config value named k.



70
71
72
73
# File 'lib/zenweb/page.rb', line 70

def [] k
  warn("#{self.url} does not define #{k.inspect}") unless config.key?(k)
  config[k]
end

#all_subpages(reversed = false) ⇒ Object

All pages below this page, possibly reversed, recursively.



78
79
80
81
82
83
# File 'lib/zenweb/page.rb', line 78

def all_subpages reversed = false
  dated, normal = subpages.partition(&:dated_path?)
  dated = dated.reverse if reversed

  (normal + dated).map { |p| [p, p.all_subpages(reversed)] }
end

#all_subpages_by_level(reversed = false) ⇒ Object

All pages below this page, possibly reversed, recursively, with the depth of each subpage relative to the current page.



89
90
91
# File 'lib/zenweb/page.rb', line 89

def all_subpages_by_level reversed = false
  self.all_subpages(reversed).deep_each.map { |n, p| [(n-1)/2, p] }
end

#analyticsObject



64
65
66
# File 'lib/zenweb/plugins/google.rb', line 64

def analytics
  [google_analytics, gauges_analytics].compact.join "\n\n"
end

#bodyObject

Returns the actual content of the file minus the optional YAML header.



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/zenweb/page.rb', line 96

def body
  # TODO: add a test for something with --- without a yaml header.
  @body ||= begin
              thing = File.file?(path) ? path : self
              _, body = Zenweb::Config.split thing
              if self.binary? then
                body
              else
                body.strip
              end
            end
end

Returns an array of all parent pages of this page, including self.



120
121
122
123
124
125
126
127
128
129
# File 'lib/zenweb/page.rb', line 120

def breadcrumbs
  pages = [self]
  loop do
    parent = pages.first.parent
    break unless parent and parent != pages.first
    pages.unshift parent
  end
  pages.pop # take self back off
  pages
end

#change_frequencyObject



195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/zenweb/page.rb', line 195

def change_frequency
  days_old = (Time.now - self.date).to_i / 86400

  case days_old
  when 0...14 then
    "daily"
  when 14...56 then
    "weekly"
  when 56...365 then
    "monthly"
  else
    "yearly"
  end
end

#clean_urlObject

Return the url as users normally enter them (ie, no index.html).



134
135
136
# File 'lib/zenweb/page.rb', line 134

def clean_url
  url.sub(/\/index.html$/, '/')
end

#configObject

Returns the closest Config instance for this file. That could be the YAML prefix in the file or it could be a _config.yml file in the file’s directory or above.



143
144
145
146
147
148
149
# File 'lib/zenweb/page.rb', line 143

def config
  unless defined? @config then
    @config = Config.new site, path
    @config = @config.parent unless content.start_with? "---"
  end
  @config
end

#contentObject

Returns the entire (raw) content of the file.



154
155
156
157
# File 'lib/zenweb/page.rb', line 154

def content
  # TODO: this has the same drawbacks as Config.split
  @content ||= File.read path
end

#dateObject

Returns either:

+ The value of the date config value + The date embedded in the filename itself (eg: 2012-01-02-blah.html). + The last modified timestamp of the file itself.



166
167
168
# File 'lib/zenweb/page.rb', line 166

def date
  config['date'] || date_from_path || File.stat(path).mtime
end

#date_from_pathObject

:nodoc:



170
171
172
173
174
# File 'lib/zenweb/page.rb', line 170

def date_from_path # :nodoc:
  # TODO: test
  date = path[/\d\d\d\d-\d\d-\d\d/]
  Time.local(*date.split(/-/).map(&:to_i)) if date
end

#date_strObject



176
177
178
179
# File 'lib/zenweb/page.rb', line 176

def date_str
  fmt ||= self.config["date_fmt"] || "%Y-%m" # REFACTOR: yuck
  self.date.strftime fmt
end

#dated?Boolean

Returns true if this page has a date (via config or within the path).

Returns:

  • (Boolean)


184
185
186
# File 'lib/zenweb/page.rb', line 184

def dated?
  config['date'] || date_from_path
end

#dated_path?Boolean

Is this a dated page? (ie, does it have YYYY-MM-DD in the path?)

Returns:

  • (Boolean)


191
192
193
# File 'lib/zenweb/page.rb', line 191

def dated_path?
  path[/\d\d\d\d[-\/]\d\d[-\/]\d\d/] || path[/\d\d\d\d(?:[-\/]\d\d)?\/index/]
end

#depends_on(deps) ⇒ Object

Wires up additional dependencies for this Page. from_deps may be a Hash (eg site.pages), an Array (eg. site.categories.blog), or a single page.



222
223
224
225
226
227
228
229
230
231
# File 'lib/zenweb/page.rb', line 222

def depends_on deps
  if String === deps then
    file self.path => deps
  else
    deps = deps.values if Hash === deps
    deps = Array(deps)

    file self.url_path => deps.map(&:url_path) - [self.url_path]
  end
end

#disqus(shortname) ⇒ Object

Returns a javascript blob to add a disqus comments block to the page.



6
7
8
9
# File 'lib/zenweb/plugins/disqus.rb', line 6

def disqus shortname
  '<div id="disqus_thread"></div>' +
    run_js_script("http://#{shortname}.disqus.com/embed.js")
end

#disqus_counts(shortname) ⇒ Object

Returns a javascript blob to convert properly formatted links to disqus comment counts.



15
16
17
# File 'lib/zenweb/plugins/disqus.rb', line 15

def disqus_counts shortname
  run_js_script "http://#{shortname}.disqus.com/count.js"
end

#erb(content, source, binding = TOPLEVEL_BINDING) ⇒ Object

Render erb in content for source with +binding.

Personally, I find erb’s delimiters a bit annoying, so for now, I’ve added additional gsub’s to the content to make it a bit more palatable.

{{ ... }} becomes <%= ... %>
{% ... %} becomes <%  ... %>

Unfortunately, those are the delimiters from liquid, so if someone goes and makes a liquid plugin it could clash. But why you’d have liquid and erb on the same file is beyond me… so it prolly won’t become a legitimate issue.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/zenweb/plugins/erb.rb', line 24

def erb content, source, binding = TOPLEVEL_BINDING
  require 'erb'
  extend ERB::Util

  unless defined? @erb then
    content = content.
      gsub(/\{\{/, "<%=").
      gsub(/\}\}/, "%>").
      gsub(/\{%/,  "<%").
      gsub(/%\}/,  "%>").
      gsub(/\\([{}%])/, '\1')

    @erb = ERB.new(content, nil, "-")
  end

  @erb.filename = source.inspect
  @erb.result binding
end

#extend_mdObject



21
22
23
# File 'lib/zenweb/plugins/markdown.rb', line 21

def extend_md
  extend Zenweb::Page::MarkdownHelpers
end

#filetype(name = self.path) ⇒ Object

Returns the extension (without the ‘.’) of name, defaulting to self.path.



237
238
239
# File 'lib/zenweb/page.rb', line 237

def filetype name = self.path
  File.extname(name)[1..-1]
end

#filetypesObject

Returns an array of extensions (in reverse order) of this page that match known renderers. For example:

Given renderer methods render_erb and render_md, the file “index.html.md.erb” would return %w[erb md], but the file “index.html” would return [].

Additional renderers can be added via Site.load_plugins.



251
252
253
254
255
# File 'lib/zenweb/page.rb', line 251

def filetypes
  @filetypes ||= path[self.class.renderers_re].split(/\./)[1..-1].reverse
rescue
  []
end

#format_date(s) ⇒ Object

Format a date string s using the config value date_fmt or YYYY/MM/DD.



260
261
262
263
# File 'lib/zenweb/page.rb', line 260

def format_date s
  fmt = self.config["date_fmt"] || "%Y/%m/%d"
  Time.local(*s.split(/-/).map(&:to_i)).strftime(fmt)
end

#gauges_analyticsObject



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/zenweb/plugins/google.rb', line 44

def gauges_analytics
  if site.config["gauges_id"] then
    <<-"EOM".gsub(/^ {8}/, '')
      <script type="text/javascript">
        var _gauges = _gauges || [];
        (function() {
          var t   = document.createElement('script');
          t.type  = 'text/javascript';
          t.async = true;
          t.id    = 'gauges-tracker';
          t.setAttribute('data-site-id', '#{site.gauges_id}');
          t.src = '//secure.gaug.es/track.js';
          var s = document.getElementsByTagName('script')[0];
          s.parentNode.insertBefore(t, s);
        })();
      </script>
    EOM
  end
end

#generateObject

Render and write the result to #url_path.



268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/zenweb/page.rb', line 268

def generate
  warn "Rendering #{url_path}"

  content = self.render

  open url_path, "w" do |f|
    if binary? then
      f.print content
    else
      f.puts content
    end
  end
end

Returns a javascript blob to add a google ad to the page. You need to provide the configuration param “google_ad_client” to your site config for this to work.



7
8
9
10
11
12
13
14
15
16
17
18
19
# File 'lib/zenweb/plugins/google.rb', line 7

def google_ad slot, width = 468, height = 60
  <<-"EOM".gsub(/^ {6}/, '')
    <script><!--
    google_ad_client = "#{self["google_ad_client"]}";
    google_ad_slot   = "#{slot}";
    google_ad_width  = #{width};
    google_ad_height = #{height};
    //-->
    </script>
    <script src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
    </script>
  EOM
end

#google_analyticsObject



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/zenweb/plugins/google.rb', line 21

def google_analytics
  if site.config["google_ua"] then
    <<-"EOM".gsub(/^ {8}/, '')
      <script type="text/javascript">
        var _gaq = _gaq || [];
        _gaq.push(['_setAccount', '#{site.google_ua}']);
        _gaq.push(['_trackPageview']);

        (function() {
        var ga = document.createElement('script');
        ga.type = 'text/javascript';
        ga.async = true;
        ga.src = ('https:' == document.location.protocol ?
                  'https://ssl' : 'http://www') +
                 '.google-analytics.com/ga.js';
        (document.getElementsByTagName('head')[0] ||
         document.getElementsByTagName('body')[0]).appendChild(ga);
        })();
      </script>
    EOM
  end
end

#html?Boolean

Returns true if this is an html page.

Returns:

  • (Boolean)


213
214
215
# File 'lib/zenweb/page.rb', line 213

def html?
  path =~ /\.html/
end

#include(name, page) ⇒ Object

Render a named file from _includes. You must pass in the current page. This can make its configuration available accessing it via page.



287
288
289
290
# File 'lib/zenweb/page.rb', line 287

def include name, page
  incl = Page.new(site, File.join("_includes", name))
  incl.subrender page
end

#index?Boolean

Returns true if this page is an index page.

Returns:

  • (Boolean)


295
296
297
# File 'lib/zenweb/page.rb', line 295

def index?
  url.end_with? "index.html"
end

#inspectObject Also known as: to_s

:nodoc:



299
300
301
# File 'lib/zenweb/page.rb', line 299

def inspect # :nodoc:
  "Page[#{path.inspect}]"
end

#layoutObject

Return a layout Page named in the config key layout.



308
309
310
311
312
313
314
315
316
# File 'lib/zenweb/page.rb', line 308

def layout
  unless defined? @layout then
    @layout = site.layout self.config["layout"]
  end
  @layout
rescue => e
  e.message.concat " for page #{path.inspect}"
  raise e
end

Convenience function to create an html link for this page.



321
322
323
# File 'lib/zenweb/page.rb', line 321

def link_html title = self.title
  %(<a href="#{clean_url}">#{title}</a>)
end

#markdown(content, no_line_numbers = false) ⇒ Object

Render markdown content.

I cheated and added some additional gsubs. I prefer ““‘ lang” so that works now.



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/zenweb/plugins/markdown.rb', line 31

def markdown content, no_line_numbers = false
  require "kramdown"
  require "coderay/zenweb_extensions"

  content = content.
   gsub(/^\`\`\` *(\w+)/) { "~~~ #$1" }.
   gsub(/^\`\`\`/, '~~~')

  config = KRAMDOWN_CONFIG.dup
  config[:coderay_line_numbers] = nil if no_line_numbers

  Kramdown::Document.new(content, config).to_html
end

#meta(key, name = key, label = "name") ⇒ Object

Stupid helper method to make declaring header meta lines cleaner



328
329
330
331
# File 'lib/zenweb/page.rb', line 328

def meta key, name=key, label="name"
  val = self.config[key]
  %(<meta #{label}="#{name}" content="#{val}">) if val
end

#no_index?Boolean

Returns:

  • (Boolean)


351
352
353
# File 'lib/zenweb/page.rb', line 351

def no_index?
  config["no_index"]
end

#parent_url(url = self.url) ⇒ Object

Returns the parent url of a particular url (or self).



112
113
114
115
# File 'lib/zenweb/page.rb', line 112

def parent_url url = self.url
  url = File.dirname url if File.basename(url) == "index.html"
  File.join File.dirname(url), "index.html"
end

#render(page = self, content = nil) ⇒ Object

Render this page as a whole. This includes rendering the page’s content into a layout if one has been specified via config.



359
360
361
362
363
364
365
366
# File 'lib/zenweb/page.rb', line 359

def render page = self, content = nil
  content = subrender page, content

  layout  = self.layout # TODO: make nullpage to avoid 'if layout' tests
  content = layout.render page, content if layout

  content
end

#render_erb(page, content) ⇒ Object

Render a page’s erb and return the result



5
6
7
# File 'lib/zenweb/plugins/erb.rb', line 5

def render_erb page, content
  erb body, self, binding
end

#render_less(page, content) ⇒ Object

Render less source to css.



5
6
7
8
9
# File 'lib/zenweb/plugins/less.rb', line 5

def render_less page, content
  require "less"

  Less::Parser.new.parse(content || body).to_css
end

#render_md(page, content) ⇒ Object

Render markdown page content using kramdown.



16
17
18
19
# File 'lib/zenweb/plugins/markdown.rb', line 16

def render_md page, content
  no_line_numbers = page.config["no_line_numbers"]
  markdown(content || self.body, no_line_numbers)
end

#run_js_script(url) ⇒ Object

TODO: move this and others to plugins/html_toys.rb (or something)



393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/zenweb/page.rb', line 393

def run_js_script url
  <<-"EOM".gsub(/^ {6}/, '')
  <script type="text/javascript">
    (function() {
      var s   = document.createElement('script');
      s.type  = 'text/javascript';
      s.async = true;
      s.src   = '#{url}';
      (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(s);
    })();
  </script>
  EOM
end

#series_pageObject



438
439
440
441
# File 'lib/zenweb/page.rb', line 438

def series_page
  series = self.config[:series]
  Zenweb::SeriesPage.all[series] if series
end

#stale?Boolean

Returns:

  • (Boolean)


368
369
370
# File 'lib/zenweb/page.rb', line 368

def stale?
  file(url_path).needed?
end

#stylesheet(name) ⇒ Object

Stupid helper method to make declaring stylesheets cleaner



375
376
377
# File 'lib/zenweb/page.rb', line 375

def stylesheet name
  %(<link rel="stylesheet" type="text/css" href="/css/#{name}.css">)
end

#subrender(page = self, content = nil) ⇒ Object

Render a Page instance based on its filetypes. For example, index.html.md.erb will essentially call:

render_md(render_erb(content))


385
386
387
388
389
# File 'lib/zenweb/page.rb', line 385

def subrender page = self, content = nil
  self.filetypes.inject(content) { |cont, type|
    send "render_#{type}", page, cont
  } || self.body
end

#tag_pagesObject



434
435
436
# File 'lib/zenweb/page.rb', line 434

def tag_pages
  (self.config[:tags] || []).map { |t| Zenweb::TagDetail.all[t] }.compact
end

#urlObject

Return the url for this page. The url is based entirely on its location in the file-system.

TODO: expand



413
414
415
416
417
418
# File 'lib/zenweb/page.rb', line 413

def url
  @url ||= self.path.
    sub(/^/, '/').
    sub(/(\d\d\d\d)-(\d\d)-(\d\d)-/) { |s| "#{format_date s}/" }.
    gsub(self.class.renderers_re, '')
end

#url_dirObject

The directory portion of the url.



423
424
425
# File 'lib/zenweb/page.rb', line 423

def url_dir
  File.dirname url_path
end

#url_pathObject

The real file path for the generated file.



430
431
432
# File 'lib/zenweb/page.rb', line 430

def url_path
  @url_path ||= File.join(".site", self.url)
end

#wireObject

Wire up this page to the rest of the rake dependencies. If you have extra dependencies for this file (ie, an index page that links to many other pages) you can add them by creating a rake task named :extra_wirings and using #depends_on. Eg:

task :extra_wirings do |x|
  site = $website
  page = site.pages

  page["sitemap.xml.erb"].    depends_on site.html_pages
  page["atom.xml.erb"].       depends_on site.pages_by_date.first(30)
  page["blog/index.html.erb"].depends_on site.categories.blog
end


458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/zenweb/page.rb', line 458

def wire
  @wired ||= false # HACK
  return if @wired
  @wired = true

  file self.path

  conf = self.config
  conf = conf.parent if self.path == conf.path

  file self.path => conf.path if conf.path
  conf.wire

  if self.layout then
    file self.path => self.layout.path
    self.layout.wire
  end

  file url_path => all_subpages.flatten.map(&:url_path) if url =~ /index.html/

  unless url_dir =~ %r%/_% then
    directory url_dir
    file url_path => url_dir
    file url_path => path do
      self.generate
    end

    task :site => url_path
  end
end