Home Manual Reference Source Test Repository

lib/url.js

import moment from 'moment';
import slug from 'slug';
import path from 'path';

/**
 * Are we running within a dist build (i.e. pre-compiled)?
 * @type {boolean} True if we're running in the dist folder.
 */
const isDistBuild = __dirname.indexOf('dist') > -1;

// Cached slug options.
let slugOptions;

/**
 * @param {string} filePath A file path.
 * @param {Array.<string>} markdownExtensions Array of known markdown file
 *   extensions.
 * @return {number} Index of the filepath extension.
 */
function getMarkdownExtensionIndexForFilePath(filePath, markdownExtensions) {
  // Get file extension of file. i.e. 'post.md' would give 'md'.
  const fileExtension = path.extname(filePath).replace(/^\./, '');
  return markdownExtensions.indexOf(fileExtension);
}

const Url = {
  /**
   * Interpolates variables into a permalink structure.
   * @example
   * // returns '/hello-world/'
   * interpolatePermalink('/:title/', {
   *   title: 'hello-world'
   * });
   * @param {string} permalink A permalink template.
   * @param {Object} context An object with keys that if matched to the
   *   permalink will have the value interpolated to the string.
   * @return {string} Actual permalink value.
   */
  interpolatePermalink(permalink, context) {
    // eslint-disable-next-line no-useless-escape
    const PERMALINK_REGEX = /:(\w+[\|A-Z]*)/g;

    const params = permalink.match(PERMALINK_REGEX);

    // If we found no tags in the permalink then just return the given string.
    if (!params) {
      return permalink;
    }

    let result = permalink;

    params.forEach(param => {
      // Replace ':title' -> 'title'.
      let paramKey = param.substr(1);

      let paramPipe;
      if (paramKey.includes('|')) {
        [paramKey, paramPipe] = paramKey.split('|');
      }

      let paramValue = context[paramKey];

      if (paramValue) {
        if (paramPipe) {
          paramValue = moment.utc(paramValue).format(paramPipe);
        }
        const sanitized = Url.slug(paramValue);
        result = result.replace(param, sanitized);
      } else {
        throw new Error(
          `${'interpolatePermalink: could not find param value ' +
            'for key: '}${paramKey}`
        );
      }
    });

    return result;
  },

  /**
   * Wrapper around the slug module. Handles taking a string and making it
   * into a slug, a URL safe string.
   * @param {string} str String to slugify.
   * @param {Object} options Slug options.
   * @return {string} Slugified string.
   */
  slug(str, options) {
    return slug(str, {
      ...slugOptions,
      options,
    });
  },

  /**
   * Set slug options to be used by Url.slug.
   * @param {Object} options Options
   */
  setSlugOptions(options) {
    slugOptions = options;
  },

  /**
   * Check if given filePath matches a markdown extension.
   * @param {string} filePath A file path.
   * @param {Array.<string>} markdownExtensions Array of known markdown file
   *   extensions.
   * @return {boolean} Whether this filePath matches any of our known markdown
   *   extensions.
   */
  pathHasMarkdownExtension(filePath, markdownExtensions) {
    return (
      getMarkdownExtensionIndexForFilePath(filePath, markdownExtensions) > -1
    );
  },

  /**
   * Replaces a markdown file path with `.html` if it's a known markdown file.
   * @param {string} filePath A file path.
   * @param {Array.<string>} markdownExtensions Array of known markdown file
   *   extensions.
   * @return {string} Modified file path.
   */
  replaceMarkdownExtension(filePath, markdownExtensions) {
    const index = getMarkdownExtensionIndexForFilePath(
      filePath,
      markdownExtensions
    );

    let result = filePath;

    // Is this file's extension one of our known markdown extensions?
    if (index > -1) {
      const foundExtension = markdownExtensions[index];
      result = filePath.replace(new RegExp(`.${foundExtension}$`), '.html');
    }

    return result;
  },

  /**
   * When writing to the file system update the permalink so that it renders
   * correctly.
   * @example
   * // returns '/hello-world/index.html'
   * Url.makeUrlFileSystemSafe('/hello-world');
   * @param {string} url Url to make file system safe.
   * @return {string} Safe url.
   */
  makeUrlFileSystemSafe(url) {
    let result = url;

    // If the url does not end with an extension then we need to modify the URL.
    if (!path.extname(url)) {
      // If we don't have a leading / then add it.
      if (!url.startsWith('/')) {
        result = `/${url}`;
      }

      // If we don't have a trailing / then add it.
      if (!url.endsWith('/')) {
        result += '/';
      }

      // Append the default file name.
      result += 'index.html';
    }

    return result;
  },

  /**
   * Given a URL that ends with 'index.html' it'll strip it off and return the
   * resulting value. Useful when creating URLs in a template.
   * @param {string} url Url to augment.
   * @return {string} Augmented url.
   */
  makePretty(url) {
    const makePrettyRegEx = /\/index.html$/;
    return url.replace(makePrettyRegEx, '/');
  },

  /**
   * Resolve a path from the root of the project, taking into account
   * the relative depth we need to go to get to the root of the project,
   * depending if we're in a pre-compiled build or not.
   * @param {...string} args Splat of strings.
   * @return {string} Full path.
   */
  pathFromRoot(...args) {
    // Push relative distance from root of project.
    args.unshift(isDistBuild ? '../../' : '../');

    // Add dirname relative from.
    args.unshift(__dirname);

    return path.resolve(...args);
  },
};

export default Url;