added support for pandoc and page properties in static markup.
authorelijah <elijah@riseup.net>
Wed, 20 Feb 2013 08:39:58 +0000 (00:39 -0800)
committerelijah <elijah@riseup.net>
Wed, 20 Feb 2013 08:39:58 +0000 (00:39 -0800)
14 files changed:
Gemfile
Gemfile.lock
app/assets/stylesheets/application.scss
app/assets/stylesheets/typography.scss
app/controllers/application_controller.rb
app/controllers/pages_controller.rb
app/helpers/application_helper.rb
app/helpers/navigation_helper.rb
app/views/layouts/application.html.haml
config/initializers/pandoc.rb [new file with mode: 0644]
lib/menu.rb
lib/property_set.rb
lib/static_page.rb
lib/template.html [new file with mode: 0644]

diff --git a/Gemfile b/Gemfile
index 7748246..2a0e0c0 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -4,7 +4,8 @@ gem 'rails', '3.2.12'
 gem 'sqlite3'
 gem 'json'
 
-gem 'markdown-rails'  # allows static pages with .md
+gem 'pandoc_rails', :path => 'vendor/gems/pandoc_rails'
+
 gem 'haml'            # allow pages with .haml
 gem 'RedCloth'        # allow :textile in HAML
 gem 'sass-rails'      # not sure why can't be in :assets group
index cfda894..f0825f6 100644 (file)
@@ -1,3 +1,10 @@
+PATH
+  remote: vendor/gems/pandoc_rails
+  specs:
+    pandoc_rails (0.0.1)
+      pandoc-ruby
+      rails
+
 GEM
   remote: https://rubygems.org/
   specs:
@@ -55,9 +62,6 @@ GEM
       i18n (>= 0.4.0)
       mime-types (~> 1.16)
       treetop (~> 1.4.8)
-    markdown-rails (0.2.1)
-      rails
-      rdiscount (>= 1.6.8, < 2.0)
     mime-types (1.21)
     multi_json (1.5.1)
     net-scp (1.1.0)
@@ -67,6 +71,7 @@ GEM
     net-ssh (2.6.5)
     net-ssh-gateway (1.2.0)
       net-ssh (>= 2.6.5)
+    pandoc-ruby (0.6.0)
     polyglot (0.3.3)
     rack (1.4.5)
     rack-cache (1.2)
@@ -91,7 +96,6 @@ GEM
       rdoc (~> 3.4)
       thor (>= 0.14.6, < 2.0)
     rake (10.0.3)
-    rdiscount (1.6.8)
     rdoc (3.12.1)
       json (~> 1.4)
     sass (3.2.5)
@@ -132,7 +136,7 @@ DEPENDENCIES
   haml
   jquery-rails
   json
-  markdown-rails
+  pandoc_rails!
   rails (= 3.2.12)
   sass-rails
   sqlite3
index 50331b4..82f7e24 100644 (file)
@@ -19,6 +19,13 @@ $fluidGridGutterWidth:    2.127659574% !default;
 // side navigation
 $side-columns: 3;
 
+//$linkColor: #2072FF;
+//$linkColor: #04c;
+//$linkVisitedColor: #40c;
+
+$linkColor: #00F;
+$linkVisitedColor: #339;
+
 //
 // IMPORT LIBRARIES
 //
index e96b02f..11519c4 100644 (file)
@@ -28,7 +28,9 @@ h2 {
 h1, h2, h3 {
   &.first {
     line-height: 0.8em;
-    margin-bottom: 0.4em
+    margin-bottom: 0.4em;
+    //font-size: 3em;
+    //color: #333;
   }
 }
 p.first {
@@ -48,3 +50,43 @@ p.first {
   line-height: 140%;
   margin: 20px 0;
 }
+
+a:visited {
+  color: $linkVisitedColor;
+}
+
+//
+// Pandoc specific HTML
+//
+
+#TOC {
+  //ul {
+  //  list-style-type: decimal;
+  // }
+  ul {
+    list-style-type: none;
+    counter-reset: level1;
+  }
+  ul li:before {
+    content: counter(level1) ". ";
+    counter-increment: level1;
+  }
+  ul li ul {
+    list-style-type: none;
+    counter-reset: level2;
+  }
+  ul li ul li:before {
+    content: counter(level1) "." counter(level2) " ";
+    counter-increment: level2;
+  }
+}
+
+a[href="#TOC"] {
+  color: black;
+  pointer-events: none;
+  cursor: default;
+  &:hover {
+    text-decoration: none;
+  }
+}
+
index b73a463..a9a25f0 100644 (file)
@@ -59,30 +59,15 @@ class ApplicationController < ActionController::Base
   # renders the content of a static page
   #
   def render_page(page)
-    begin
-      render :template => page.template_path
-    rescue ActionView::MissingTemplate => exc
-      begin
-        render :template => page.template_path(DEFAULT_LOCALE)
-      rescue
-        raise exc
-      end
-    end
+    render :text => page.render_to_string(self), :layout => true
   end
+  helper_method :render_page
 
   #
-  # same as render page, but returns the string
+  # same as render page, but returns the string without the layout
   #
   def page_body(page)
-    begin
-      render_to_string :template => page.template_path
-    rescue ActionView::MissingTemplate => exc
-      begin
-        render_to_string :template => page.template_path(DEFAULT_LOCALE)
-      rescue
-        raise exc
-      end
-    end
+    page.render_to_string(self)
   end
   helper_method :page_body
 
@@ -114,4 +99,5 @@ class ApplicationController < ActionController::Base
   #  Thread.current[key_name] = true
   #end
 
+
 end
index acf525c..3e76092 100644 (file)
@@ -14,6 +14,7 @@ class PagesController < ApplicationController
         format.atom { render_atom_feed(@page) }
       end
     else
+      logger.error("ERROR: could not find page %s" % params[:page])
       raise PageNotFound.new
     end
   end
index 2933bd1..45e8228 100644 (file)
@@ -69,13 +69,9 @@ module ApplicationHelper
 
   def page_title
     if @page
-      if @page.props
-        @page.props.title || @page.title
-      else
-        @page.title
-      end
+      @page.title
     else
-      ""
+      nil
     end
   end
 
@@ -85,16 +81,16 @@ module ApplicationHelper
   # they call two different 'render' methods (controller and view renders behave differently).
   # TODO: figure out how to combine into one helper_method.
   #
-  def render_page(page)
-    begin
-      render :template => page.template_path
-    rescue ActionView::MissingTemplate => exc
-      begin
-        render :template => page.template_path(DEFAULT_LOCALE)
-      rescue
-        raise exc
-      end
-    end
-  end
+  def render_page(page)
+    begin
+      render :template => page.template_path
+    rescue ActionView::MissingTemplate => exc
+      begin
+        render :template => page.template_path(DEFAULT_LOCALE)
+      rescue
+        raise exc
+      end
+    end
+  end
 
 end
index fde6b45..b4ca514 100644 (file)
@@ -19,7 +19,7 @@ module NavigationHelper
       site.menu.each do |item|
         active = current_page_path.first == item.name ? 'active' : ''
         haml 'li.tab', :class => first do
-          haml 'a.tab', I18n.t('pages.' + item.name), :href => menu_item_path(item), :class => active
+          haml 'a.tab', menu_item_title(item), :href => menu_item_path(item), :class => active
         end
         first = ''
       end
@@ -39,7 +39,7 @@ module NavigationHelper
   def act_as(page)
     page = site.find_page(page)
     @current_page_path = page.path
-    render_page(page)
+    page_body(page)
   end
 
   private
@@ -48,12 +48,24 @@ module NavigationHelper
     "/#{I18n.locale}/#{item.path.join('/')}"
   end
 
+  def menu_item_title(item)
+    page = site.pages[item.path_str] || site.pages[item.name]
+    if page
+      page.nav_title(I18n.locale)
+    else
+      nil
+    end
+  end
+
   def display_menu(menu, level=0)
     menu.each do |item|
-      haml 'li', :class => path_active(current_page_path, item) do
-        haml 'a', I18n.t('pages.'+item.name), :href => menu_item_path(item), :class => "level#{level}"
+      title = menu_item_title(item)
+      if title
+        haml 'li', :class => path_active(current_page_path, item) do
+          haml 'a', menu_item_title(item), :href => menu_item_path(item), :class => "level#{level}"
+        end
+        display_menu(item.submenu, level+1)
       end
-      display_menu(item.submenu, level+1)
     end
   end
 
index 3d28f38..b85c6c4 100644 (file)
@@ -1,7 +1,8 @@
 !!! 5
 %html{:dir=>'ltr'}
   %head
-    %title #{site.title} - #{page_title}
+    %title
+      = [page_title, site.title].compact.join(' - ')
     = stylesheet_link_tag    "application", :media => "all"
     = javascript_include_tag "application"
     = csrf_meta_tags
diff --git a/config/initializers/pandoc.rb b/config/initializers/pandoc.rb
new file mode 100644 (file)
index 0000000..a1f948a
--- /dev/null
@@ -0,0 +1,2 @@
+
+PandocRails.register
\ No newline at end of file
index 4da8441..b586151 100644 (file)
@@ -50,6 +50,10 @@ class Menu
     end
   end
 
+  def path_str
+    @path_str ||= path.join('/')
+  end
+
   def each(&block)
     children.each(&block)
   end
index 64982c9..05aa46a 100644 (file)
@@ -11,6 +11,7 @@
 # getting the property
 #
 #   page.props.title
+#   page.props.locale('en').title
 #
 
 require 'i18n'
@@ -39,15 +40,17 @@ class PropertySet
     def textile(str)
       RedCloth.new(str).to_html
     end
-    def get(var_name)
+    def get(var_name, inheritance=true)
       value = instance_variable_get("@#{var_name}")
       if value.nil?
         if @_locale != DEFAULT_LOCALE
           # try value from default locale
           @_ps.get_var(var_name)
-        else
+        elsif inheritance
           # try inherited value
           @_ps.get_inherited_var(var_name, @_locale)
+        else
+          nil
         end
       else
         value
@@ -66,7 +69,9 @@ class PropertySet
   #
   # evaluate the template_string, and load the variables defined into an AttrObject.
   #
-  def eval(locale, template_string)
+  def eval(template_string, locale)
+    locale ||= DEFAULT_LOCALE
+
     # render to the template to get the instance variables
     attrs = AttrObject.new(self, locale)
     begin
@@ -136,6 +141,18 @@ class PropertySet
   end
 
   #
+  # like get_var, but forbits inheritance
+  #
+  def get_var_without_inheritance(var_name, locale=I18n.locale)
+    attrs = locale(locale)
+    if attrs
+      attrs.get(var_name, false)
+    else
+      nil
+    end
+  end
+
+  #
   # tries to get the value of an inherited variable
   #
   def get_inherited_var(var_name, locale=I18n.locale)
index eb32319..b4bc40d 100644 (file)
@@ -1,4 +1,4 @@
-#
+  #
 # class StaticPage
 #
 # represents a static website page.
@@ -8,9 +8,11 @@
 require 'property_set'
 require 'i18n'
 require 'pathname'
+require 'RedCloth'
+require 'pandoc-ruby'
 
 class StaticPage
-  attr_accessor :path, :children, :name, :file_path, :props, :parent, :mount_point
+  attr_accessor :path, :children, :name, :file_path, :props, :parent, :mount_point, :locales
 
   ##
   ## CLASS METHODS
@@ -31,7 +33,8 @@ class StaticPage
   end
 
   #
-  # loads a directory, creating StaticPages from the directory structure
+  # loads a directory, creating StaticPages from the directory structure,
+  # yielding each StaticPage as it is created.
   #
   def scan(&block)
     Dir.chdir(file_path) do
@@ -41,7 +44,7 @@ class StaticPage
           yield child
           child.scan(&block)
         elsif is_simple_page?(child_name)
-          child = StaticPage.new(self, file_without_suffix(child_name))
+          child = StaticPage.new(self, child_name)
           yield child
         end
       end
@@ -54,7 +57,17 @@ class StaticPage
 
   def initialize(parent, name, file_path=nil)
     @children = []
-    @name = name
+
+    # set the @name and @suffix
+    @suffix = File.extname(name)
+    if @suffix.chars.any?
+      @name = file_without_suffix(name)
+    else
+      @name = name
+      @suffix = nil
+    end
+
+    # set @parent & @path
     if parent
       @parent = parent
       @mount_point = @parent.mount_point
@@ -63,6 +76,8 @@ class StaticPage
     else
       @path = []
     end
+
+    # set the @file_path
     if file_path
       @file_path = file_path
     elsif @parent && @parent.file_path
@@ -70,8 +85,13 @@ class StaticPage
     else
       raise 'file path must be specified or in parent'
     end
+
+    # discover supported locales
     @simple_page = !File.directory?(@file_path)
-    @props = load_properties
+    #@locales = find_locales()
+
+    # eval the property headers, if any
+    @props = load_properties()
   end
 
   def add_child(page)
@@ -100,15 +120,69 @@ class StaticPage
     end
   end
 
+  #
+  # e.g. pages/about-us/contact/en
+  #
+  def absolute_template_path(locale=I18n.locale)
+    if @simple_page
+      "#{@mount_point.directory}/#{@path.join('/')}"
+    else
+      "#{@mount_point.directory}/#{@path.join('/')}/#{locale}"
+    end
+  end
+
+  #
+  # e.g. pages/about-us/contact/en.haml
+  #
+  #def source_path(locale=I18n.locale)
+  #  if @simple_page
+  #
+  #  else
+  #
+  #  end
+  #end
+
   def inspect
     "<'#{@path.join('/')}' #{children.inspect}>"
   end
 
-  def title
+  #
+  # title is tricky
+  #
+  # * for nav_title, default to title, then try to inherit.
+  # * for title, default to nav_title, then try to inherit.
+  #
+  def title(locale=I18n.locale)
+    title = props.get_var_without_inheritance(:title, locale)
+    title ||= props.get_var_without_inheritance(:nav_title, locale)
+    title ||= props.get_var(:title, locale)
+    title ||= props.get_var(:nav_title, locale)
+    title ||= I18n.t('pages.'+@name, :raise => false, :default => "Untitled", :locale => locale)
+    return title
+  end
+
+  def nav_title(locale=I18n.locale)
+    return_value = I18n.t('pages.'+@name, :raise => false, :default => "Untitled", :locale => locale)
+    if return_value == "Untitled"
+      property_title = props.get_var_without_inheritance(:nav_title, locale)
+      property_title ||= props.get_var_without_inheritance(:title, locale)
+      property_title ||= props.get_var(:nav_title, locale)
+      property_title ||= props.get_var(:title, locale)
+      return_value = property_title || return_value
+    end
+    return_value
+  end
+
+  def render_to_string(renderer)
     begin
-      I18n.t!('pages.' + @name, :raise => true)
-    rescue I18n::MissingTranslationData
-      props.title
+      render_locale(renderer, I18n.locale)
+    rescue ActionView::MissingTemplate, MissingTemplate => exc
+      begin
+        render_locale(renderer, DEFAULT_LOCALE)
+      rescue
+        Rails.logger.error "ERROR: could not file template path #{self.template_path}"
+        raise exc
+      end
     end
   end
 
@@ -124,30 +198,49 @@ class StaticPage
 
   private
 
-  #def self.relative_to_rails_view_root(absolute_path)
-  #  if Rails.root
-  #    absolute = Pathname.new(absolute_path)
-  #    rails_view_root = Pathname.new(Rails.root + 'app/views')
-  #    absolute.relative_path_from(rails_view_root).to_s
-  #  end
-  #end
+  ##
+  ## PROPERTIES
+  ##
 
+  #
+  # scans the source content files for property headers in the form:
+  #
+  #    @variable = 'x'
+  #    - @variable = 'x'
+  #
+  # (with or without leading hypen works)
+  #
+  # this text is extracted and evaluated as ruby to set properties.
+  #
   def load_properties
     props = PropertySet.new(self)
-    Dir.glob(file_path + '/*.haml') do |content_file_path|
-      locale = File.basename(content_file_path).sub(File.extname(content_file_path),'')
-      #variable_header = ""
-      #File.open(content_file_path) do |f|
-      #  while (line = f.gets) =~ /^- @/
-      #    variable_header << line
-      #  end
-      #end
-      props.eval(locale, File.read(content_file_path))
+    content_files.each do |content_file, locale|
+      if File.extname(content_file) == '.haml'
+        props.eval(File.read(content_file), locale)
+      else
+        headers = []
+        File.open(content_file) do |f|
+          while (line = f.gets) =~ /^(- |)@\w/
+            if line !~ /^-/
+              line = '- ' + line
+            end
+            headers << line
+          end
+        end
+        props.eval(headers.join("\n"), locale)
+      end
     end
     return props
   end
 
-  SUFFIXES = '(haml|md)'
+  ##
+  ## CONTENT FILES
+  ##
+
+  SUFFIXES = '(haml|md|markdown|txt|textile|rst|latex|pandoc|html)'
+
+  # e.g. en.haml or es.md
+  LOCALE_FILE_MATCH = /^(#{AVAILABLE_LANGUAGES.join('|')})\.#{SUFFIXES}$/
 
   #
   # returns true if the name of a file could be a 'simple' static page
@@ -158,12 +251,137 @@ class StaticPage
   # * we exclude file names that are locales.
   #
   def is_simple_page?(name)
-    name =~ /\.#{SUFFIXES}$/ && name !~ /^(#{AVAILABLE_LANGUAGES.join('|')})\.#{SUFFIXES}$/
+    name =~ /\.#{SUFFIXES}$/ && name !~ LOCALE_FILE_MATCH
   end
 
   def file_without_suffix(name)
     name.sub(/^(.*?)\.#{SUFFIXES}$/, "\\1")
   end
+
+  #
+  # returns an array like so:
+  #
+  #  [
+  #     ['/path/to/page/en.haml', 'en']
+  #     ['/path/to/page/es.haml', 'es']
+  #  ]
+  #
+  # Or this, if page is simple:
+  #
+  # [
+  #   ['/path/to/page.haml', nil]
+  # ]
+  #
+  #
+  def content_files
+    if @simple_page
+      [[@file_path + @suffix,nil]]
+    elsif File.directory?(@file_path)
+      Dir.foreach(@file_path).collect { |file|
+        if file && file =~ LOCALE_FILE_MATCH
+          [File.join(@file_path, file), $1]
+        end
+      }.compact
+    end
+  end
+
+  #def self.relative_to_rails_view_root(absolute_path)
+  #  if Rails.root
+  #    absolute = Pathname.new(absolute_path)
+  #    rails_view_root = Pathname.new(Rails.root + 'app/views')
+  #    absolute.relative_path_from(rails_view_root).to_s
+  #  end
+  #end
+
+  ##
+  ## RENDERING
+  ##
+
+  PROPERTY_HEADER = /^\s*(^(|- )@\w[^\n]*?\n)*/m
+
+  class MissingTemplate < StandardError
+  end
+
+  def render_locale(renderer, locale)
+    if is_haml_template?(locale)
+      renderer.render_to_string(:template => self.template_path(locale), :layout => false).html_safe
+    else
+      render_static_locale(locale).html_safe
+    end
+  end
+
+  def render_static_locale(locale)
+    content_files.each do |content_file, file_locale|
+      if file_locale.nil? || locale == file_locale
+        return render_content_file(content_file, locale)
+      end
+    end
+    raise MissingTemplate.new(template_path(locale))
+  end
+
+  #
+  # todo: maybe use RDiscount for markdown instead?
+  #
+  def render_content_file(content_file, locale)
+    content = File.read(content_file).sub(PROPERTY_HEADER, '')
+    suffix = File.extname(content_file)
+    if PANDOC_FORMATS[suffix]
+      render_pandoc(content, suffix, locale)
+    elsif REDCLOTH_FORMATS[suffix]
+      render_redcloth(content, suffix, locale)
+    else
+      "sorry, i don't understand how to render #{suffix}"
+    end
+  end
+
+  def is_haml_template?(locale)
+    @suffix == '.haml' || File.exists?(self.absolute_template_path(locale) + '.haml')
+  end
+
+  PANDOC_FORMATS = {
+    '.md'       => :markdown,
+    '.markdown' => :markdown,
+    #'.txt'      => :textile,
+    #'.textile'  => :textile,
+    '.rst'      => :rst,
+    '.latex'    => :latex,
+    '.pandoc'   => :pandoc,
+  }
+
+  def render_pandoc(string, suffix, locale)
+    args = [string, {:from => PANDOC_FORMATS[suffix], :to => :html5}, "smart"]
+    if props.locale(locale).toc != false
+      args << "table_of_contents"
+      args << {"template" => "'#{File.dirname(__FILE__) + '/template.html'}'"}
+    end
+    unless (title = explicit_title(locale)).nil?
+      args << {"variable" => "title:'#{title}'"}
+    end
+    renderer = PandocRuby.new(*args)
+    renderer.convert
+  end
+
+  #
+  # pandoc can do textile, but it does it HORRIBLY
+  #
+  REDCLOTH_FORMATS = {
+    '.txt'      => :textile,
+    '.textile'  => :textile,
+  }
+
+  def render_redcloth(string, suffix, locale)
+    unless (title = explicit_title(locale)).nil?
+      string = "h1. #{title}\n\n" + string
+    end
+    RedCloth.new(string).to_html
+  end
+
+  #
+  # returns title iff explicitly set.
+  #
+  def explicit_title(locale)
+    props.get_var_without_inheritance(:title, locale)
+  end
 end
 
 
diff --git a/lib/template.html b/lib/template.html
new file mode 100644 (file)
index 0000000..379a602
--- /dev/null
@@ -0,0 +1,9 @@
+$if(title)$
+<h1 class="first">$title$</h1>
+$endif$
+$if(toc)$
+<div id="$idprefix$TOC">
+$toc$
+</div>
+$endif$
+$body$
\ No newline at end of file