middleman/middleman-blog

View on GitHub
lib/middleman-blog/extension.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true

require 'active_support/core_ext/time/zones'
require 'middleman-blog/blog_data'
require 'middleman-blog/blog_article'
require 'middleman-blog/helpers'

module Middleman
  class BlogExtension < Extension
    extend Forwardable

    self.supports_multiple_instances = true

    def_delegator :app, :logger

    option :name, nil, 'Unique ID for telling multiple blogs apart'
    option :prefix, nil, 'Prefix to mount the blog at (modifies permalink, sources, taglink, year_link, month_link, day_link to start with the prefix)'
    option :permalink, '/{year}/{month}/{day}/{title}.html', 'Path articles are generated at. Tokens can be omitted or duplicated, and you can use tokens defined in article frontmatter.'
    option :sources, '{year}-{month}-{day}-{title}.html', 'Pattern for matching source blog articles (no template extensions)'
    option :taglink, 'tags/{tag}.html', 'Path tag pages are generated at.'
    option :layout, 'layout', 'Article-specific layout'
    option :summary_separator, /(READMORE)/, 'Regex or string that delimits the article summary from the rest of the article.'
    option :summary_length, 250, 'Truncate summary to be <= this number of characters. Set to -1 to disable summary truncation.'
    option :summary_generator, nil, 'A block that defines how summaries are extracted. It will be passed the rendered article content, max summary length, and ellipsis string as arguments.'
    option :year_link, '/{year}.html', 'Path yearly archive pages are generated at.'
    option :month_link, '/{year}/{month}.html', 'Path monthly archive pages are generated at.'
    option :day_link, '/{year}/{month}/{day}.html', 'Path daily archive pages are generated at.'
    option :calendar_template, nil, 'Template path (no template extension) for calendar pages (year/month/day archives).'
    option :year_template, nil, 'Template path (no template extension) for yearly archive pages. Defaults to the :calendar_template.'
    option :month_template, nil, 'Template path (no template extension) for monthly archive pages. Defaults to the :calendar_template.'
    option :day_template, nil, 'Template path (no template extension) for daily archive pages. Defaults to the :calendar_template.'
    option :tag_template, nil, 'Template path (no template extension) for tag archive pages.'
    option :generate_year_pages, true, 'Whether to generate year pages.'
    option :generate_month_pages, true, 'Whether to generate month pages.'
    option :generate_day_pages, true, 'Whether to generate day pages.'
    option :generate_tag_pages, true, 'Whether to generate tag pages.'
    option :paginate, false, 'Whether to paginate lists of articles'
    option :per_page, 10, 'Number of articles per page when paginating'
    option :filter, nil, 'A proc that can be used to select articles that should be published based on user-defined criteria'
    option :page_link, 'page/{num}', 'Path to append for additional pages when paginating'
    option :publish_future_dated, false, 'Whether articles with a date in the future should be considered published'
    option :custom_collections, {}, 'Hash of custom frontmatter properties to collect articles on and their options (link, template)'
    option :preserve_locale, false, 'Use the global Middleman I18n.locale instead of the lang in the article\'s frontmatter'
    option :new_article_template, File.expand_path('commands/article.tt', __dir__), 'Path (relative to project root) to an ERb template that will be used to generate new articles from the "middleman article" command.'
    option :default_extension, '.markdown', 'Default template extension for articles (used by "middleman article")'

    # @return [BlogData] blog data for this blog, which has all information about the blog articles
    attr_reader :data

    # @return [Symbol] the name of this blog (autogenerated if not provided).
    attr_reader :name

    # @return [TagPages] tag page handler for this blog
    attr_reader :tag_pages

    # @return [CalendarPages] calendar page handler for this blog
    attr_reader :calendar_pages

    # @return [Paginator] pagination handler for this blog
    attr_reader :paginator

    # @return [Hash<CustomPages>] custom pages handlers for this blog, indexed by property name
    attr_reader :custom_pages

    # Helpers for use within templates and layouts.
    self.defined_helpers = [Middleman::Blog::Helpers]

    def initialize(app, options_hash = {}, &block)
      super

      @custom_pages = {}

      # NAME is the name of this particular blog, and how you reference it from #blog_controller or frontmatter.
      @name = options.name.to_sym if options.name

      # Allow one setting to set all the calendar templates
      if options.calendar_template
        options.year_template  ||= options.calendar_template
        options.month_template ||= options.calendar_template
        options.day_template   ||= options.calendar_template
      end

      # If "prefix" option is specified, all other paths are relative to it.
      return unless options.prefix

      options.prefix = "/#{options.prefix}" unless options.prefix.start_with? '/'
      options.permalink = File.join(options.prefix, options.permalink)
      options.sources = File.join(options.prefix, options.sources)
      options.taglink = File.join(options.prefix, options.taglink)
      options.year_link = File.join(options.prefix, options.year_link)
      options.month_link = File.join(options.prefix, options.month_link)
      options.day_link = File.join(options.prefix, options.day_link)

      options.custom_collections.each_value do |opts|
        opts[:link] = File.join(options.prefix, opts[:link])
      end
    end

    def after_configuration
      @name ||= begin
        found_name = nil

        app.extensions[:blog].values.each_with_index do |ext, i|
          found_name = "blog#{i + 1}" if ext == self
        end

        found_name
      end

      # TODO: break up into private methods?

      @app.ignore(options.calendar_template) if options.calendar_template
      @app.ignore(options.year_template) if options.year_template
      @app.ignore(options.month_template) if options.month_template
      @app.ignore(options.day_template) if options.day_template
      @app.ignore options.tag_template if options.tag_template

      # Make sure ActiveSupport's TimeZone stuff has something to work with,
      # allowing people to set their desired time zone via Time.zone or
      # set :time_zone
      Time.zone = app.config[:time_zone] if app.config[:time_zone]
      time_zone = Time.zone || 'UTC'
      zone_default = Time.find_zone!(time_zone)
      raise 'Value assigned to time_zone not recognized.' unless zone_default

      Time.zone_default = zone_default

      # Initialize blog with options
      @data = Blog::BlogData.new(@app, self, options)

      @app.sitemap.register_resource_list_manipulator(:"blog_#{name}_articles", @data)

      if options.tag_template
        @app.ignore options.tag_template

        require 'middleman-blog/tag_pages'
        @tag_pages = Blog::TagPages.new(@app, self)
        @app.sitemap.register_resource_list_manipulator(:"blog_#{name}_tags", @tag_pages)
      end

      if options.year_template || options.month_template || options.day_template
        require 'middleman-blog/calendar_pages'
        @calendar_pages = Blog::CalendarPages.new(@app, self)
        @app.sitemap.register_resource_list_manipulator(:"blog_#{name}_calendar", @calendar_pages)
      end

      if options.custom_collections
        require 'middleman-blog/custom_pages'
        register_custom_pages
      end

      if options.paginate
        require 'middleman-blog/paginator'
        @paginator = Blog::Paginator.new(@app, self)
        @app.sitemap.register_resource_list_manipulator(:"blog_#{name}_paginate", @paginator)
      end

      logger.info "== Blog Sources: #{options.sources} (:prefix + :sources)"
    end

    private

    # Register any custom page collections that may be set in the config
    #
    # A custom resource list manipulator will be generated for each key in the
    # custom collections hash.
    #
    # The following will collect posts on the "category" frontmatter property:
    #   ```
    #   activate :blog do |blog|
    #     blog.custom_collections = {
    #       category: {
    #         link: "/categories/:category.html",
    #         template: "/category.html"
    #       }
    #     }
    #   end
    #   ```
    #
    # Category pages in the example above will use the category.html as a template file
    # and it will be ignored when building.
    def register_custom_pages
      options.custom_collections.each do |property, options|
        @app.ignore options[:template]

        @custom_pages[property] = Blog::CustomPages.new(property, @app, self, options)
        @app.sitemap.register_resource_list_manipulator(:"blog_#{name}_#{property}", @custom_pages[property])

        Blog::Helpers.generate_custom_helper(property)
      end
    end
  end
end