umbrelllla
6/11/2014 - 12:26 PM

product_generator.rb

#------------------------------------------------------------------------
# encoding: utf-8
# @(#)product_generator.rb	1.00 29-Nov-2011 16:38
#
# Copyright (c) 2011 Jim Pravetz. All Rights Reserved.
# Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php)
#
# Description:  A generator that creates product, products and
#		ingredients pages for jekyll sites.  Uses a JSON data
#		file as the database file from which to read and
#		generate the above files.
#
# Included filters : (none)
#
# Available _config.yml settings :
# - product_title_prefix   An optional name to prefix product titles with (default '')
# - category_meta_description_prefix An optional product metadata prefix (default 'Product: ')
# - products_src_dir:      The source subfolder from where product, products and ingredients pages are obtained.
# - products_dir:          The subfolder to build products pages in (default is 'products').
# - data_dir:              The subfolder under source where data files are read.
# - data_product_file:     The name of the JSON object file within the data_dir.
#
# Update History: (most recent first)
#  22-Dec-2011 jpravetz -- Added sitemap support
#  18-Dec-2011 jpravetz -- Added Page.to_liquid method to properly set page.url
#   5-Dec-2011 jpravetz -- Split product.options into an array
#  29-Nov-2011 jpravetz -- Created from category_generator.rb
#------------------------------------------------------------------------

require 'json'

module Jekyll

  # The ProductPage class creates a single ingredients, product, or products page
  class ProductPage < Page
    
    # The resultant relative URL of where the published file will end up
    # Added for use by a sitemap generator
    attr_accessor :dest_url
    # The last modified date to be used for web caching of this file.
    # Derived from latest date of products.json and template files
    # Added for use by a sitemap generator
    attr_accessor :src_mtime

    # Initialize a new Page.
    #
    # site - The Site object.
    # base - The String path to the source.
    # dest_dir  - The String path between the dest and the file.
    # dest_name - The String name of the destination file (e.g. index.html or myproduct.html)
    # src_dir  - The String path between the source and the file.
    # src_name - The String filename of the source page file, minus the markdown or html extension
    # data_mtime - mtime of the products.json data file, used for sitemap generator
    def initialize(site, base, dest_dir, dest_name, src_dir, src_name, data_mtime )
      @site = site
      @base = base
      @dir  = dest_dir
      @dest_dir = dest_dir
      @dest_name = dest_name
      @dest_url = File.join( '/', dest_dir ) 
      @dest_url = File.join( '/', dest_dir, dest_name ) if !dest_name.match( /index.html/i )
      @src_mtime = data_mtime

      src_file = File.join(base, src_dir, "#{src_name}.markdown" )
      src_name_with_ext = "#{src_name}.markdown" if File.exists?( src_file )
      src_name_with_ext ||= "#{src_name}.html"
      
      @name = src_name_with_ext
      self.process(src_name_with_ext)
      
      # Read the YAML from the specified page
      self.read_yaml(File.join(base, src_dir), src_name_with_ext )
      
      # Remember the mod time, used for site_map
      file_mtime = File.mtime( File.join(base, src_dir, src_name_with_ext) )
      @src_mtime = file_mtime if file_mtime > @src_mtime
    end

    # Override to set url properly
    def to_liquid
      self.data.deep_merge({
        "url"        => @dest_url,
        "content"    => self.content })
    end

    # Attach our  data to the global page variable. This allows pages to see this data.
    # Use to set ingredients or products.
    def set_data( label, data )
      self.data[label] = data
    end

    # Attach our  data to the global page variable. This allows pages to see this data.
    # Use to set product. Also sets the page title.
    def set_product_data( product, prev_product, next_product )
      self.data['product'] = product
      self.data['product_prev'] = prev_product
      self.data['product_next'] = next_product
      title = product['title']
      # Set the title for this page.
      title_prefix             = site.config['product_title_prefix'] || ''
      self.data['title']       = "#{title_prefix}#{title}"
      # Set the meta-description for this page.
      meta_description_prefix  = site.config['category_meta_description_prefix'] || 'Product: '
      self.data['description'] = "#{meta_description_prefix}#{product['title']}"
    end

    # Override so that we can control where the destination file goes
    def destination(dest)
      # The url needs to be unescaped in order to preserve the correct filename.
      path = File.join(dest, @dest_dir, @dest_name )
      path = File.join(path, "index.html") if self.url =~ /\/$/
      path
    end

  end

  # The Site class is a built-in Jekyll class with access to global site config information.
  # It is not necessary to extend the Site class, it just convenient to do so. And 
  # category_generator.rb did this, so it must be a good idea.
  class Site

    # Creates instances of ProductPage, renders then, and writes the output to a file.
    # Will create a page for products index, ingredients index and each product.
    def write_all_product_files

      # Read the JSON file. This is our 'database'. We read obtain
      # this object by running a command (hooked up as a Rake task) to
      # read the data from a google spreadsheet via Google's APIs.
      json_filename = self.config['data_product_file'] # || 'products.json'
      data_hash = read_data_object( json_filename ) # if File.exists?( json_filename )
      json_mtime = data_hash['mtime'] if data_hash
      data = data_hash['data'] if data_hash
      ingredients = data['ingredients'] if data
      products = data['products'] if data
      unless ingredients && products
        return "Products or Ingredients not found in data file"
      end
      puts "## Products file read: found #{products.length} products and #{ingredients.length} ingredients"
      
      # Translate any values that need translating before we pass the data off to liquid processing
      # new_products = clean_products( products )
     
      # This folder contains three source pages: ingredients,
      # products, product. These three pages are used to build the
      # public pages and can have a markdown or html extension
      products_src_dir = self.config['products_src_dir'] || '_products'

      # Write out all our pages
      write_ingredients_index( ingredients, products_src_dir, 'ingredients', json_mtime )
      write_products_index( products, products_src_dir, 'products', json_mtime )
      write_product_indexes( products, products_src_dir, 'products', json_mtime )
    end

    # Write an ingredients/index.html page
    def write_ingredients_index( data, products_src_dir, dest_dir, data_mtime )
      index = ProductPage.new( self, self.config['source'], dest_dir, 'index.html', products_src_dir, 'ingredients', data_mtime )
      index.set_data( 'ingredients', data )
      index.render(self.layouts, site_payload)
      # puts "## self.dest = #{self.dest}"
      index.write(self.dest)
      # Record the fact that this page has been added, otherwise Site::cleanup will remove it.
      self.pages << index
    end

    # Write a products/index.html page
    def write_products_index( data, products_src_dir, dest_dir, data_mtime )
      index = ProductPage.new( self, self.config['source'], dest_dir, 'index.html', products_src_dir, 'products', data_mtime )
      index.set_data( 'products', data )
      index.render(self.layouts, site_payload)
      index.write(self.dest)
      # Record the fact that this page has been added, otherwise Site::cleanup will remove it.
      self.pages << index
    end

    # Loops through the list of product pages and processes each one.
    def write_product_indexes( products, products_dir, dest_dir, data_mtime )
      if products && products.length > 0
        if self.layouts.key? 'page'
          products.each_with_index do |product,index|
            write_product_page( product, products_dir, dest_dir, (index > 0 ) ? products[index-1] : nil, products[index+1], data_mtime ) if product['publish']
          end
        else
          throw "No 'product' layout found."
        end
      end
    end

    # Write a products/product-name/index.html page
    def write_product_page( product, products_src_dir, dest_dir, prev_product, next_product, data_mtime )
      # Attach our product data to global site variable. This allows pages to see this product's data.
      puts "## Processing product #{product['id']}"
      # puts "## Previous/next products: #{prev_product ? prev_product['id'] : 'nil'}/#{next_product ? next_product['id'] : 'nil'}"
      index = ProductPage.new( self, self.config['source'], File.join(dest_dir,product['id']), 'index.html', products_src_dir, 'product', data_mtime )
      index.set_product_data( product, prev_product, next_product )
      index.render(self.layouts, site_payload)
      index.write(self.dest)
      # Record the fact that this page has been added, otherwise Site::cleanup will remove it.
      self.pages << index
    end


    # Read and parse the JSON file under the data directory
    # +filename+ is the String name of the file to be read
    def read_data_object( filename )

      data_dir = self.config['data_dir'] || '_data'
      data_path = File.join(self.config['source'], data_dir)
      if File.symlink?(data_path)
        return "Data directory '#{data_path}' cannot be a symlink"
      end
      file = File.join(data_path, filename)

      return "File #{file} could not be found" if !File.exists?( file )
      
      result = nil
      Dir.chdir(data_path) do
        result = File.read( filename )
      end
      puts "## Error: No data in #{file}" if result.nil?
      # puts result
      result = JSON.parse( result ) if result
      { 'data' => result,
        'mtime' => File.mtime(file) }
    end

  end


  # Jekyll hook - the generate method is called by jekyll, and generates all of the product pages.
  class ProductGenerator < Generator
    safe true

    def generate(site)
      site.write_all_product_files
    end
  end

end