Home Manual Reference Source Test Repository

lib/collection/type/metadata.js

import _ from 'lodash';

import Url from '../../url';
import CollectionBase from '../base';

/**
 * A collection that derives its content from a match in a files yaml
 * frontmatter data.
 */
export default class MetadataCollection extends CollectionBase {
  constructor(name, collectionConfig, config, renderer) {
    super(name, collectionConfig, config, renderer);

    /**
     * Object which holds a mapping of metadata value to the files that contain
     * the metadata property.
     * For example with metadata of 'tags' you'd have:
     * {
     *  'tag-name': [file, file],
     *  'other-tag': [file, file]
     * }
     * @type {Object.<string, Array.<File>>}
     */
    this.metadataFiles = Object.create(null);
  }

  /**
   * Checks to see if this file passes all requirements to be considered a part
   * of this collection.
   * @param {File} file File object.
   * @return {boolean} true if the file meets all requirements.
   */
  _isFileInCollection(file) {
    return !_.isUndefined(file.data[this.metadata]) && !this.isFiltered(file);
  }

  /**
   * Add a file to the collection.
   * @param {File} file File object.
   * @return {boolean} True if the file was added to the collection.
   */
  addFile(file) {
    if (!this._isFileInCollection(file)) {
      return false;
    }

    let metadataValues = file.data[this.metadata];
    if (!Array.isArray(metadataValues)) {
      metadataValues = [metadataValues];
    }

    metadataValues.forEach(rawValue => {
      // Slugify each value to make sure there's no collisions when we write
      // CollectionPages to disk. This prevents `open source` and `open-source`
      // from being in two different keys but both writing to the same
      // file-system destination.
      const value = _.isString(rawValue) ? Url.slug(rawValue) : rawValue;
      this.metadataFiles[value] = this.metadataFiles[value] || [];
      this.metadataFiles[value].push(file);

      // Add data to template accessible object.
      this.data.metadata[value] = this.data.metadata[value] || [];
      this.data.metadata[value].push(file.data);
    });

    return true;
  }

  /**
   * Populate the Collection's files via file system path or metadata attribute.
   * @param {Object.<string, Files>} files Object of files.
   * @return {Collection}
   */
  populate(files) {
    // Create metadata files.
    this.metadataFiles = {};

    // Initialize template data.
    this.data.metadata = {};

    // Store files that are in our collection.
    _.each(files, file => {
      // Don't return value so we iterate over every file.
      this.addFile(file);
    });

    this.createCollectionPages();

    return this;
  }

  /**
   * Create CollectionPage objects for our Collection.
   * @return {boolean} True if we successfully created CollectionPages.
   * @private
   */
  createCollectionPages() {
    // If no permalink paths are set then we don't render a CollectionPage.
    if (!(this.permalink && this.permalink.index && this.permalink.page)) {
      return false;
    }

    if (!_.isEmpty(this.metadataFiles)) {
      this.pages = [];

      // Create CollectionPage objects to represent our pagination pages.
      _.each(this.metadataFiles, (rawFiles, metadataKey) => {
        // Sort files.
        const files = CollectionBase.sortFiles(
          rawFiles,
          this.sort,
          this._config.get('file.dateFormat')
        );

        // Break up our array of files into arrays that match our defined
        // pagination size.
        const pages = _.chunk(files, this.pageSize);

        pages.forEach((pageFiles, index) => {
          // Create CollectionPage.
          const collectionPage = this.createPage(
            index,
            `${this.id}:${metadataKey}:${index}` // Custom ID.
          );

          collectionPage.setData({
            // Extra template information.
            metadata: metadataKey,

            // How many pages in the collection.
            total_pages: pages.length,

            // Posts displayed per page
            per_page: this.pageSize,

            // Total number of posts
            total: files.length,
          });

          // Files in the page.
          collectionPage.setFiles(pageFiles);

          // Create a map of the metadataKey to its full URL on the file object.
          // Useful when rendering and wanting to link out to the metadata page.
          pageFiles.forEach(file => {
            file.data.metadataUrls = file.data.metadataUrls || {};
            file.data.metadataUrls[metadataKey] = collectionPage.data.url;
          });

          // Add to our array of pages.
          this.pages.push(collectionPage);
        });
      });
    }

    this._linkPages(
      // ShouldLinkPrevious
      (previous, collectionPage) =>
        // With metadata collections all pages aren't made in the same context.
        // i.e. for a tag metadata collection you'll have 3 pages with metadata
        // value of 'review', and 2 pages of value 'tutorial'. These different
        // metadata values should not be linked.
        previous &&
        this.metadataFiles &&
        previous.data.metadata === collectionPage.data.metadata,
      // ShouldLinkNext
      (next, collectionPage) =>
        // With metadata collections all pages aren't made in the same context.
        // i.e. for a tag metadata collection you'll have 3 pages with metadata
        // value of 'review', and 2 pages of value 'tutorial'. These different
        // metadata values should not be linked.
        next &&
        this.metadataFiles &&
        next.data.metadata === collectionPage.data.metadata
    );

    return true;
  }
}