module Nanoc::DataSource::Filesystem
class FileProxy
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
def initialize(path)
@path = path
end
def method_missing(sym, *args, &block)
File.new(@path).__send__(sym, *args, &block)
end
end
class FilesystemDataSource < Nanoc::DataSource
########## Attributes ##########
identifier :filesystem
########## Preparation ##########
def up
end
def down
end
def setup
# Create page
FileManager.create_file 'content/content.txt' do
"I'm a brand new root page. Please edit me!\n"
end
FileManager.create_file 'content/content.yaml' do
"# Built-in\n" +
"\n" +
"# Custom\n" +
"title: \"A New Root Page\"\n"
end
# Create page defaults
FileManager.create_file 'meta.yaml' do
"# This file contains the default values for all metafiles.\n" +
"# Other metafiles can override the contents of this one.\n" +
"\n" +
"# Built-in\n" +
"custom_path: none\n" +
"extension: \"html\"\n" +
"filename: \"index\"\n" +
"filters_post: []\n" +
"filters_pre: []\n" +
"is_draft: false\n" +
"layout: \"default\"\n" +
"skip_output: false\n" +
"\n" +
"# Custom\n"
end
# Create template
FileManager.create_file 'templates/default/default.txt' do
"Hi, I'm a new page!\n"
end
FileManager.create_file 'templates/default/default.yaml' do
"# Built-in\n" +
"\n" +
"# Custom\n" +
"title: \"A New Page\"\n"
end
# Create layout
FileManager.create_file 'layouts/default.erb' do
"\n" +
"
\n" +
" <%= @page.title %>\n" +
" \n" +
" \n" +
"<%= @page.content %>\n" +
" \n" +
"\n"
end
# Create code
FileManager.create_file 'lib/default.rb' do
"\# All files in the 'lib' directory will be loaded\n" +
"\# before nanoc starts compiling.\n" +
"\n" +
"def html_escape(str)\n" +
" str.gsub('&', '&').str('<', '<').str('>', '>').str('\"', '"')\n" +
"end\n" +
"alias h html_escape\n"
end
end
########## Loading data ##########
# The filesystem data source stores its pages in nested directories. Each
# directory represents a single page. The root directory is the 'content'
# directory.
#
# Every directory has a content file and a meta file. The content file
# contains the actual page content, while the meta file contains the
# page's metadata.
#
# Both content files and meta files are named after its parent directory
# (i.e. page). For example, a page named 'foo' will have a directory named
# 'foo', with e.g. a 'foo.markdown' content file and a 'foo.yaml' meta
# file.
#
# Content file extensions are ignored by nanoc. The content file extension
# does not determine the filters to run on it; the meta file defines the
# list of filters. The meta file extension must always be 'yaml', though.
#
# Content files can also have the 'index' basename. Similarly, meta files
# can have the 'meta' basename. For example, a parent directory named
# 'foo' can have an 'index.txt' content file and a 'meta.yaml' meta file.
# This is to preserve backward compatibility.
def pages
meta_filenames.inject([]) do |pages, filename|
# Read metadata
meta = (YAML.load_file(filename) || {}).clean
if meta[:is_draft]
# Skip drafts
pages
else
# Get extra info
path = filename.sub(/^content/, '').sub(/[^\/]+\.yaml$/, '')
file = content_file_for_dir(File.dirname(filename))
extras = {
:path => path,
:file => FileProxy.new(file.path),
:uncompiled_content => file.read
}
# Add to list of pages
pages + [ meta.merge(extras) ]
end
end
end
# The page defaults are loaded from a 'meta.yaml' file
def page_defaults
(YAML.load_file('meta.yaml') || {}).clean
end
# Layouts are stored as files in the 'layouts' directory. Each layout has
# a basename (the part before the extension) and an extension. Unlike page
# content files, the extension _is_ used for determining the layout
# processor; which extension maps to which layout processor is defined in
# the layout processors.
def layouts
Dir["layouts/*"].reject { |f| f =~ /~$/ }.map do |filename|
# Get layout details
extension = File.extname(filename)
name = File.basename(filename, extension)
content = File.read(filename)
# Build hash for layout
{ :name => name, :content => content, :extension => extension }
end
end
# Templates are located in the 'templates' directroy. Every template is a
# directory consisting of a content file and a meta file, both named after
# the template. This is very similar to the way pages are stored, except
# that templates cannot be nested.
def templates
meta_filenames('templates').inject([]) do |templates, filename|
# Get template name
name = filename.sub(/^templates\/(.*)\/[^\/]+\.yaml$/, '\1')
# Get file names
meta_filename = filename
content_filenames = Dir['templates/' + name + '/' + name + '.*'] +
Dir['templates/' + name + '/index.*'] -
Dir['templates/' + name + '/*.yaml' ]
# Read files
extension = nil
content = nil
content_filenames.each do |content_filename|
content = File.read(content_filename)
extension = File.extname(content_filename)
end
meta = File.read(meta_filename)
# Add it to the list of templates
templates + [{
:name => name,
:extension => extension,
:content => content,
:meta => meta
}]
end
end
# Code is stored in '.rb' files in the 'lib' directory. Code can reside
# in sub-directories.
def code
Dir['lib/**/*.rb'].sort.inject('') { |m, f| m + File.read(f) + "\n" }
end
########## Creating data ##########
# Creating a page creates a page directory with the name of the page in
# the 'content' directory, as well as a content file named xxx.txt and a
# meta file named xxx.yaml (with xxx being the name of the page).
def create_page(path, template)
# Make sure path does not start or end with a slash
sanitized_path = path.gsub(/^\/+|\/+$/, '')
# Get paths
dir_path = 'content/' + sanitized_path
name = sanitized_path.sub(/.*\/([^\/]+)$/, '\1')
meta_path = dir_path + '/' + name + '.yaml'
content_path = dir_path + '/' + name + template[:extension]
# Make sure the page doesn't exist yet
error "A page named '#{path}' already exists." if File.exist?(meta_path)
# Create index and meta file
FileManager.create_file(meta_path) { template[:meta] }
FileManager.create_file(content_path) { template[:content] }
end
# Creating a layout creates a single file in the 'layouts' directory,
# named xxx.erb (with xxx being the name of the layout).
def create_layout(name)
# Get details
path = 'layouts/' + name + '.erb'
# Make sure the layout doesn't exist yet
error "A layout named '#{name}' already exists." if File.exist?(path)
# Create layout file
FileManager.create_file(path) do
"\n" +
" \n" +
" <%= @page.title %>\n" +
" \n" +
" \n" +
"<%= @page.content %>\n" +
" \n" +
"\n"
end
end
# Creating a template creates a template directory with the name of the
# template in the 'templates' directory, as well as a content file named
# xxx.txt and a meta file named xxx.yaml (with xxx being the name of the
# template).
def create_template(name)
# Get paths
meta_path = 'templates/' + name + '/' + name + '.yaml'
content_path = 'templates/' + name + '/' + name + '.txt'
# Make sure the template doesn't exist yet
error "A template named '#{name}' already exists." if File.exist?(meta_path)
# Create index and meta file
FileManager.create_file(meta_path) { "# Built-in\n\n# Custom\ntitle: A New Page\n" }
FileManager.create_file(content_path) { "Hi, I'm new here!\n" }
end
private
########## Custom functions ##########
# Returns the list of meta files in the given (optional) base directory.
def meta_filenames(base='content')
# Find all possible meta file names
filenames = Dir[base + '/**/*.yaml']
# Filter out invalid meta files
good_filenames = []
bad_filenames = []
filenames.each do |filename|
if filename =~ /meta\.yaml$/ or filename =~ /([^\/]+)\/\1\.yaml$/
good_filenames << filename
else
bad_filenames << filename
end
end
# Warn about bad filenames
unless bad_filenames.empty?
error "The following files appear to be meta files, " +
"but have an invalid name:\n - " +
bad_filenames.join("\n - ")
end
good_filenames
end
# Returns a File object for the content file in the given directory
def content_file_for_dir(dir)
# Find all files
filename_glob_1 = dir.sub(/([^\/]+)$/, '\1/\1.*')
filename_glob_2 = dir.sub(/([^\/]+)$/, '\1/index.*')
filenames = Dir[filename_glob_1] + Dir[filename_glob_2]
# Reject meta files
filenames.reject! { |f| f =~ /\.yaml$/ }
# Reject backups
filenames.reject! { |f| f =~ /~$/ }
# Make sure there is only one content file
if filenames.size != 1
error "Expected 1 content file in #{dir} but found #{filenames.size}"
end
# Read content file
File.new(filenames.first)
end
end
end