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,
  :hard_wrap     => false,
  :input         => "GFM",
  :gfm_quirks    => "no_auto_typographic",

  :syntax_highlighter => :coderay,
  :syntax_highlighter_opts => {
    :wrap         => :div,
    :line_numbers => :table,
    :tab_width    => 4,
    :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.



344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/zenweb/page.rb', line 344

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
42
43
44
45
# 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 = if RUBY_VERSION >= "2.6.0" then
             ERB.new(content, trim_mode:"-")
           else
             ERB.new(content, nil, "-")
           end
  end

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

#extend_mdObject



26
27
28
# File 'lib/zenweb/plugins/markdown.rb', line 26

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
# File 'lib/zenweb/page.rb', line 308

def layout
  unless defined? @layout then
    @layout = site.layout self.config["layout"]
  end
  @layout
rescue => e
  raise e.exception "%s for page %p" % [e.message, path]
end

Stupid helper method to make declaring header link lines cleaner



335
336
337
# File 'lib/zenweb/page.rb', line 335

def link_head **kws
  %(<link #{kws.map { |k,v| "#{k}=#{v.inspect}" }.join " "} />)
end

Convenience function to create an html link for this page.



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

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.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/zenweb/plugins/markdown.rb', line 36

def markdown content, no_line_numbers = false
  require "kramdown"
  require "kramdown-parser-gfm"
  require "kramdown-syntax-coderay"
  require "coderay/zenweb_extensions"

  config = KRAMDOWN_CONFIG.dup
  if no_line_numbers then
    config[:syntax_highlighter_opts] = config[:syntax_highlighter_opts].dup
    config[:syntax_highlighter_opts][:line_numbers] = nil
  end

  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



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

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

#no_index?Boolean

Returns:

  • (Boolean)


357
358
359
# File 'lib/zenweb/page.rb', line 357

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.



365
366
367
368
369
370
371
372
# File 'lib/zenweb/page.rb', line 365

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.



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

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)



399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/zenweb/page.rb', line 399

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



444
445
446
447
# File 'lib/zenweb/page.rb', line 444

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

#stale?Boolean

Returns:

  • (Boolean)


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

def stale?
  file(url_path).needed?
end

#stylesheet(name) ⇒ Object

Stupid helper method to make declaring stylesheets cleaner



381
382
383
# File 'lib/zenweb/page.rb', line 381

def stylesheet name
  link_head 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))


391
392
393
394
395
# File 'lib/zenweb/page.rb', line 391

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

#tag_pagesObject



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

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



419
420
421
422
423
424
# File 'lib/zenweb/page.rb', line 419

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.



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

def url_dir
  File.dirname url_path
end

#url_pathObject

The real file path for the generated file.



436
437
438
# File 'lib/zenweb/page.rb', line 436

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


464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
# File 'lib/zenweb/page.rb', line 464

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