lib/middleman-core/util/data.rb



require 'yaml'
require 'json'
require 'pathname'
require 'backports/2.1.0/array/to_h'
require 'hashie'
require 'memoist'

require 'middleman-core/util/binary'
require 'middleman-core/contracts'

module Middleman
  module Util
    include Contracts

    module_function

    class EnhancedHash < ::Hashie::Mash
      # include ::Hashie::Extensions::MergeInitializer
      # include ::Hashie::Extensions::MethodReader
      # include ::Hashie::Extensions::IndifferentAccess
    end

    # Recursively convert a normal Hash into a EnhancedHash
    #
    # @private
    # @param [Hash] data Normal hash
    # @return [Hash]
    Contract Any => Maybe[Or[Array, EnhancedHash]]
    def recursively_enhance(obj)
      if obj.is_a? ::Array
        obj.map { |e| recursively_enhance(e) }
      elsif obj.is_a? ::Hash
        EnhancedHash.new(obj)
      else
        obj
      end
    end

    module Data
      extend Memoist
      include Contracts

      module_function

      # Get the frontmatter and plain content from a file
      # @param [String] path
      # @return [Array<Hash, String>]
      Contract IsA['Middleman::SourceFile'], Maybe[Symbol] => [Hash, Maybe[String]]
      def parse(file, frontmatter_delims, known_type=nil)
        full_path = file[:full_path]
        return [{}, nil] if ::Middleman::Util.binary?(full_path) || file[:types].include?(:binary)

        # Avoid weird race condition when a file is renamed
        begin
          content = file.read
        rescue EOFError, IOError, ::Errno::ENOENT
          return [{}, nil]
        end

        match = build_regex(frontmatter_delims).match(content) || {}

        unless match[:frontmatter]
          case known_type
          when :yaml
            return [parse_yaml(content, full_path), nil]
          when :json
            return [parse_json(content, full_path), nil]
          end
        end

        case [match[:start], match[:stop]]
        when *frontmatter_delims[:yaml]
          [
            parse_yaml(match[:frontmatter], full_path),
            match[:additional_content]
          ]
        when *frontmatter_delims[:json]
          [
            parse_json("{#{match[:frontmatter]}}", full_path),
            match[:additional_content]
          ]
        else
          [
            {},
            content
          ]
        end
      end

      def build_regex(frontmatter_delims)
        start_delims, stop_delims = frontmatter_delims
                                    .values
                                    .flatten(1)
                                    .transpose
                                    .map(&::Regexp.method(:union))

        /
          \A(?:[^\r\n]*coding:[^\r\n]*\r?\n)?
          (?<start>#{start_delims})[ ]*\r?\n
          (?<frontmatter>.*?)[ ]*\r?\n?
          ^(?<stop>#{stop_delims})[ ]*\r?\n?
          \r?\n?
          (?<additional_content>.*)
        /mx
      end
      memoize :build_regex

      # Parse YAML frontmatter out of a string
      # @param [String] content
      # @return [Hash]
      Contract String, Pathname => Hash
      def parse_yaml(content, full_path)
        c = begin
          ::Middleman::Util.instrument 'parse.yaml' do
            ::YAML.load(content)
          end
        rescue StandardError, ::Psych::SyntaxError => error
          warn "YAML Exception parsing #{full_path}: #{error.message}"
          {}
        end
      
        c ? symbolize_recursive(c) : {}
      end
      memoize :parse_yaml

      # Parse JSON frontmatter out of a string
      # @param [String] content
      # @return [Hash]
      Contract String, Pathname => Hash
      def parse_json(content, full_path)
        c = begin
          ::Middleman::Util.instrument 'parse.json' do
            ::JSON.parse(content)
          end
        rescue StandardError => error
          warn "JSON Exception parsing #{full_path}: #{error.message}"
          {}
        end

        c ? symbolize_recursive(c) : {}
      end
      memoize :parse_json

      def symbolize_recursive(value)
        case value
        when Hash
          value.map do |k, v|
            key = k.is_a?(String) ? k.to_sym : k
            [key, symbolize_recursive(v)]
          end.to_h
        when Array
          value.map { |v| symbolize_recursive(v) }
        else
          value
        end
      end
    end
  end
end