module Nanoc::DataSources::Filesystem

Provides functionality common across all filesystem data sources.

@api private

Public Instance Methods

content_dir_name() click to toggle source
# File lib/nanoc/data_sources/filesystem.rb, line 14
def content_dir_name
  config.fetch(:content_dir, 'content')
end
down() click to toggle source

See {Nanoc::DataSource#down}.

# File lib/nanoc/data_sources/filesystem.rb, line 11
def down
end
items() click to toggle source

See {Nanoc::DataSource#items}.

# File lib/nanoc/data_sources/filesystem.rb, line 23
def items
  load_objects(content_dir_name, 'item', Nanoc::Int::Item)
end
layouts() click to toggle source

See {Nanoc::DataSource#layouts}.

# File lib/nanoc/data_sources/filesystem.rb, line 28
def layouts
  load_objects(layouts_dir_name, 'layout', Nanoc::Int::Layout)
end
layouts_dir_name() click to toggle source
# File lib/nanoc/data_sources/filesystem.rb, line 18
def layouts_dir_name
  config.fetch(:layouts_dir, 'layouts')
end
up() click to toggle source

See {Nanoc::DataSource#up}.

# File lib/nanoc/data_sources/filesystem.rb, line 7
def up
end

Protected Instance Methods

all_files_in(dir_name) click to toggle source

Returns all files in the given directory and directories below it.

# File lib/nanoc/data_sources/filesystem.rb, line 156
def all_files_in(dir_name)
  Nanoc::Extra::FilesystemTools.all_files_in(dir_name, config[:extra_files])
end
all_split_files_in(dir_name) click to toggle source

e.g.

{
  'content/foo' => [ 'yaml', ['html', 'md'] ],
  'content/bar' => [ 'yaml', [nil]          ],
  'content/qux' => [ nil,    ['html']       ]
}
# File lib/nanoc/data_sources/filesystem.rb, line 122
def all_split_files_in(dir_name)
  by_basename =
    all_files_in(dir_name)
    .reject   { |fn| fn =~ /(~|\.orig|\.rej|\.bak)$/ }
    .group_by { |fn| basename_of(fn) }

  all = {}

  by_basename.each_pair do |basename, filenames|
    # Divide
    meta_filenames    = filenames.select { |fn| ext_of(fn) == '.yaml' }
    content_filenames = filenames.select { |fn| ext_of(fn) != '.yaml' }

    # Check number of files per type
    unless [0, 1].include?(meta_filenames.size)
      raise "Found #{meta_filenames.size} meta files for #{basename}; expected 0 or 1"
    end
    unless config[:identifier_type] == 'full'
      unless [0, 1].include?(content_filenames.size)
        raise "Found #{content_filenames.size} content files for #{basename}; expected 0 or 1"
      end
    end

    all[basename] = []
    all[basename][0] =
      meta_filenames[0] ? 'yaml' : nil
    all[basename][1] =
      content_filenames.any? ? content_filenames.map { |fn| ext_of(fn)[1..-1] || '' } : [nil]
  end

  all
end
basename_of(filename) click to toggle source

Returns the base name of filename, i.e. filename with the first or all extensions stripped off. By default, all extensions are stripped off, but when allow_periods_in_identifiers is set to true in the site configuration, only the last extension will be stripped .

# File lib/nanoc/data_sources/filesystem.rb, line 188
def basename_of(filename)
  filename.sub(extension_regex, '')
end
ext_of(filename) click to toggle source

Returns the extension(s) of filename. Supports multiple extensions. Includes the leading period.

# File lib/nanoc/data_sources/filesystem.rb, line 194
def ext_of(filename)
  filename =~ extension_regex ? Regexp.last_match[1] : ''
end
extension_regex() click to toggle source

Returns a regex that is used for determining the extension of a file name. The first match group will be the entire extension, including the leading period.

# File lib/nanoc/data_sources/filesystem.rb, line 201
def extension_regex
  if @config && @config[:allow_periods_in_identifiers]
    /(\.[^\/\.]+$)/
  else
    /(\.[^\/]+$)/
  end
end
filename_for(_base_filename, _ext) click to toggle source

Returns the filename for the given base filename and the extension.

If the extension is nil, this function should return nil as well.

A simple implementation would simply concatenate the base filename, a period and an extension (which is what the {Nanoc::DataSources::FilesystemCompact} data source does), but other data sources may prefer to implement this differently (for example, {Nanoc::DataSources::FilesystemVerbose} doubles the last part of the basename before concatenating it with a period and the extension).

# File lib/nanoc/data_sources/filesystem.rb, line 170
def filename_for(_base_filename, _ext)
  raise NotImplementedError.new(
    "#{self.class} does not implement #filename_for",
  )
end
identifier_for_filename(_filename) click to toggle source

Returns the identifier that corresponds with the given filename, which can be the content filename or the meta filename.

# File lib/nanoc/data_sources/filesystem.rb, line 178
def identifier_for_filename(_filename)
  raise NotImplementedError.new(
    "#{self.class} does not implement #identifier_for_filename",
  )
end
load_objects(dir_name, kind, klass) click to toggle source

Creates instances of klass corresponding to the files in dir_name. The kind attribute indicates the kind of object that is being loaded and is used solely for debugging purposes.

This particular implementation loads objects from a filesystem-based data source where content and attributes can be spread over two separate files. The content and meta-file are optional (but at least one of them needs to be present, obviously) and the content file can start with a metadata section.

@see #load_objects

# File lib/nanoc/data_sources/filesystem.rb, line 45
def load_objects(dir_name, kind, klass)
  res = []

  return [] if dir_name.nil?

  all_split_files_in(dir_name).each do |base_filename, (meta_ext, content_exts)|
    content_exts.each do |content_ext|
      # Get filenames
      meta_filename    = filename_for(base_filename, meta_ext)
      content_filename = filename_for(base_filename, content_ext)

      # Read content and metadata
      is_binary = content_filename && !@site_config[:text_extensions].include?(File.extname(content_filename)[1..-1])
      if is_binary && klass == Nanoc::Int::Item
        meta                = (meta_filename && YAML.load_file(meta_filename)) || {}
        content_or_filename = content_filename
      elsif is_binary && klass == Nanoc::Int::Layout
        raise "The layout file '#{content_filename}' is a binary file, but layouts can only be textual"
      else
        meta, content_or_filename = parse(content_filename, meta_filename, kind)
      end

      # Get attributes
      attributes = {
        filename: content_filename,
        content_filename: content_filename,
        meta_filename: meta_filename,
        extension: content_filename ? ext_of(content_filename)[1..-1] : nil,
      }.merge(meta)

      # Get identifier
      if content_filename
        identifier = identifier_for_filename(content_filename[dir_name.length..-1])
      elsif meta_filename
        identifier = identifier_for_filename(meta_filename[dir_name.length..-1])
      else
        raise 'meta_filename and content_filename are both nil'
      end

      # Get modification times
      meta_mtime = meta_filename ? File.stat(meta_filename).mtime : nil
      content_mtime = content_filename ? File.stat(content_filename).mtime : nil
      if meta_mtime && content_mtime
        mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
      elsif meta_mtime
        mtime = meta_mtime
      elsif content_mtime
        mtime = content_mtime
      else
        raise 'meta_mtime and content_mtime are both nil'
      end
      attributes[:mtime] = mtime

      # Create content
      full_content_filename = content_filename && File.expand_path(content_filename)
      content =
        if is_binary
          Nanoc::Int::BinaryContent.new(full_content_filename)
        else
          Nanoc::Int::TextualContent.new(content_or_filename, filename: full_content_filename)
        end

      # Create object
      res << klass.new(content, attributes, identifier)
    end
  end

  res
end
parse(content_filename, meta_filename, _kind) click to toggle source

Parses the file named `filename` and returns an array with its first element a hash with the file's metadata, and with its second element the file content itself.

# File lib/nanoc/data_sources/filesystem.rb, line 212
def parse(content_filename, meta_filename, _kind)
  # Read content and metadata from separate files
  if meta_filename
    content = content_filename ? read(content_filename) : ''
    meta_raw = read(meta_filename)
    begin
      meta = YAML.load(meta_raw) || {}
    rescue Exception => e
      raise "Could not parse YAML for #{meta_filename}: #{e.message}"
    end
    verify_meta(meta, meta_filename)
    return [meta, content]
  end

  # Read data
  data = read(content_filename)

  # Check presence of metadata section
  if data !~ /\A-{3,5}\s*$/
    return [{}, data]
  end

  # Split data
  pieces = data.split(/^(-{5}|-{3})[ \t]*\r?\n?/, 3)
  if pieces.size < 4
    raise RuntimeError.new(
      "The file '#{content_filename}' appears to start with a metadata section (three or five dashes at the top) but it does not seem to be in the correct format.",
    )
  end

  # Parse
  begin
    meta = YAML.load(pieces[2]) || {}
  rescue Exception => e
    raise "Could not parse YAML for #{content_filename}: #{e.message}"
  end
  verify_meta(meta, content_filename)
  content = pieces[4]

  # Done
  [meta, content]
end
raise_encoding_error(filename, encoding) click to toggle source

Raises an invalid encoding error for the given filename and encoding.

# File lib/nanoc/data_sources/filesystem.rb, line 306
def raise_encoding_error(filename, encoding)
  raise RuntimeError.new("Could not read #{filename} because the file is not valid #{encoding}.")
end
read(filename) click to toggle source

Reads the content of the file with the given name and returns a string in UTF-8 encoding. The original encoding of the string is derived from the default external encoding, but this can be overridden by the “encoding” configuration attribute in the data source configuration.

# File lib/nanoc/data_sources/filesystem.rb, line 271
def read(filename)
  # Read
  begin
    data = File.read(filename)
  rescue => e
    raise RuntimeError.new("Could not read #{filename}: #{e.inspect}")
  end

  # Fix
  if data.respond_to?(:encode!)
    if @config && @config[:encoding]
      original_encoding = Encoding.find(@config[:encoding])
      data.force_encoding(@config[:encoding])
    else
      original_encoding = data.encoding
    end

    begin
      data.encode!('UTF-8')
    rescue
      raise_encoding_error(filename, original_encoding)
    end

    unless data.valid_encoding?
      raise_encoding_error(filename, original_encoding)
    end
  end

  # Remove UTF-8 BOM (ugly)
  data.delete!("\xEF\xBB\xBF")

  data
end
verify_meta(meta, filename) click to toggle source
# File lib/nanoc/data_sources/filesystem.rb, line 261
def verify_meta(meta, filename)
  return if meta.is_a?(Hash)

  raise InvalidMetadataError.new(filename, meta.class)
end