all files / lib/ file.js

95.74% Statements 90/94
73.33% Branches 22/30
88.89% Functions 8/9
92.16% Lines 47/51
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217     46×         46×                                                                 46×     47×           47×                                                                       47×               54×               45× 45×               92× 92× 92×     91×     37×       54×     54×   54×       54×       54× 54×             92×                     92×                                                                            
import crypto from 'crypto';
import path from 'path';
import fs from 'fs';
import isUndefined from 'lodash/isUndefined';
import merge from 'lodash/merge';
import isNil from 'lodash/isNil';
import isString from 'lodash/isString';
import log from './log';
import Url from './url';
import Parse from './parse';
import Render from './render';
 
export default class File {
  constructor(filePath = '', getConfig = () => {}) {
    /**
     * Absolute path to file location.
     * @type {string}
     */
    this.path = filePath;
 
    /**
     * Unique ID for this file. Right now an alias for the file's path.
     * @type {string}
     */
    this.id = this.path;
 
    /**
     * Template accessible data.
     * @type {Object.<string, Object>}
     */
    this.data = Object.create(null);
 
    /**
     * An array of collection IDs that this file belongs to.
     * @type {Set.<string>}
     */
    this.collectionIds = new Set();
 
    /**
     * An array of CollectionPage IDs that this file belongs to.
     * @type {Set.<string>}
     */
    this.pageIds = new Set();
 
    /**
     * Get the global config object.
     * @type {Function}
     * @private
     */
    this._getConfig = getConfig;
 
    this.updateDataFromFileSystem();
  }
 
  updateDataFromFileSystem() {
    /**
     * Raw contents of file, directly from file system.
     * @TODO: perhaps don't keep reference to this?
     * @type {string} One long string.
     */
    this.rawContent = fs.readFileSync(this.path, 'utf8');
 
    /**
     * Checksum hash of rawContent, for use in seeing if file is different.
     * @example:
     * 	'50de70409f11f87b430f248daaa94d67'
     * @type {string}
     */
    this.checksum = crypto.createHash('md5')
      .update(this.rawContent, 'utf8').digest('hex');
 
    // Parse file's frontmatter.
    let parsedFile = Parse.fromFrontMatter(this.rawContent);
 
    /**
     * Just the file's text content.
     * @type {string}
     */
    let content = parsedFile.content;
 
    /**
     * If the file itself wants to customize what its URL is then it will use
     * the `config.file.url_key` value of the File's frontmatter as the basis
     * for which the URL of this file should be.
     * So if you have a File with a frontmatter that has `url: /pandas/` then
     * the File's URL will be `/pandas/`.
     * @type {string?} url Relative path to file.
     */
    this.url = parsedFile.data[this._getConfig().get('file.url_key')];
 
    // Merge in new data that's accessible from template.
    merge(this.data, parsedFile.data, {
      // The content of the Page.
      content: content
    });
 
    this._calculateDestination();
  }
 
  /**
   * Path to file relative to root of project.
   * @type {string}
   */
  get pathRelative() {
    return this.path.replace(this._getConfig().get('path.source'), '');
  }
 
  /**
   * Set new permalink configuration this file should use.
   * @param {string} newPermalink New permalink.
   */
  setPermalink(newPermalink) {
    this.permalink = newPermalink;
    this._calculateDestination();
  }
 
  /**
   * Calculate both relative and absolute destination path for where to write
   * the file.
   * @private
   */
  _calculateDestination() {
    let destinationUrl;
    if (this.url) {
      // If the individual File defined its own unique URL that gets first
      // dibs at setting the official URL for this file.
      destinationUrl = this.url;
    } else if (this.permalink) {
      // If the file has no URL but has a permalink set on it then use it to
      // find the URL of the File.
      destinationUrl = Url.interpolatePermalink(
        this.permalink,
        this.data
      );
    } else E{
      // If the file has no URL set and no permalink then use its relative file
      // path as its url.
      destinationUrl = this.pathRelative;
 
      const markdownExtensions = this._getConfig().get('markdown_extension');
 
      // Get file extension of file. i.e. 'post.md' would give 'md'.
      let fileExtension = path.extname(destinationUrl).replace(/^\./, '');
      let index = markdownExtensions.indexOf(fileExtension);
 
      // Is this file's extension one of our known markdown extensions?
      if (index > -1) {
        let foundExtension = markdownExtensions[index];
        destinationUrl = destinationUrl.replace(
          new RegExp(`.${foundExtension}$`),
          '.html'
        );
      }
    }
 
    let fileSystemSafeDestination = Url.makeUrlFileSystemSafe(destinationUrl);
 
    /**
     * Absolute destination path.
     * @type {string} destination Absolute path to file.
     */
    this.destination = path.join(
      this._getConfig().get('path.destination'),
      fileSystemSafeDestination
    );
 
    this.data.url = Url.makePretty(fileSystemSafeDestination);
  }
 
  render(template, globalData) E{
    template = isUndefined(this.data.template) ? template : this.data.template;
    let result = this.data.content;
 
    let templateData = {
      ...globalData,
      file: this.data
    };
 
    try {
      // Set result of content to result content.
      result = Render.fromTemplateString(
        this.data.content,
        templateData
      );
 
      // Set result to file's contents.
      this.data.content = result;
    } catch (e) {
      log.error(e.message);
      throw new Error(`File: Could not render file's contents.\n` +
        `File: ${JSON.stringify(this)}`);
    }
 
    // Convert to HTML.
    // Howeve if the File's frontmatter sets markdown value to false then
    // skip the markdown conversion.
    if (this.data.markdown !== false) {
      result = Render.fromMarkdown(this.data.content);
      this.data.content = result;
    }
 
    Eif (!isNil(template) && !(isString(template) && template.length === 0)) {
      try {
        result = Render.fromTemplate(template, templateData);
      } catch (e) {
        if (e.message.includes(Render.TemplateErrorMessage.NO_TEMPLATE)) {
          throw new Error(`File: Template '${template}' not found.\n` +
            `File: ${JSON.stringify(this)}`);
        }
      }
    }
 
    return result;
  }
 
}