264 lines
8.2 KiB
JavaScript
264 lines
8.2 KiB
JavaScript
/*
|
|
* decaffeinate suggestions:
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS103: Rewrite code to no longer use __guard__
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
/**
|
|
Definition of the FRESHTheme class.
|
|
@module core/fresh-theme
|
|
@license MIT. See LICENSE.md for details.
|
|
*/
|
|
|
|
|
|
|
|
const FS = require('fs');
|
|
const validator = require('is-my-json-valid');
|
|
const _ = require('underscore');
|
|
const PATH = require('path');
|
|
const parsePath = require('parse-filepath');
|
|
const pathExists = require('path-exists').sync;
|
|
const EXTEND = require('extend');
|
|
const HMSTATUS = require('./status-codes');
|
|
const moment = require('moment');
|
|
const loadSafeJson = require('../utils/safe-json-loader');
|
|
const READFILES = require('recursive-readdir-sync');
|
|
|
|
|
|
|
|
/* A representation of a FRESH theme asset.
|
|
@class FRESHTheme */
|
|
class FRESHTheme {
|
|
|
|
constructor() {
|
|
this.baseFolder = 'src';
|
|
}
|
|
|
|
/* Open and parse the specified theme. */
|
|
open( themeFolder ) {
|
|
|
|
this.folder = themeFolder;
|
|
|
|
// Open the [theme-name].json file; should have the same name as folder
|
|
const pathInfo = parsePath(themeFolder);
|
|
|
|
// Set up a formats hash for the theme
|
|
let formatsHash = { };
|
|
|
|
// Load the theme
|
|
const themeFile = PATH.join(themeFolder, 'theme.json');
|
|
const themeInfo = loadSafeJson(themeFile);
|
|
if (themeInfo.ex) {
|
|
throw{
|
|
fluenterror:
|
|
themeInfo.ex.op === 'parse'
|
|
? HMSTATUS.parseError
|
|
: HMSTATUS.readError,
|
|
inner: themeInfo.ex.inner
|
|
};
|
|
}
|
|
|
|
const that = this;
|
|
|
|
// Move properties from the theme JSON file to the theme object
|
|
EXTEND(true, this, themeInfo.json);
|
|
|
|
// Check for an "inherits" entry in the theme JSON.
|
|
if (this.inherits) {
|
|
const cached = { };
|
|
_.each(this.inherits, function(th, key) {
|
|
// First, see if this is one of the predefined FRESH themes. There are
|
|
// only a handful of these, but they may change over time, so we need to
|
|
// query the official source of truth: the fresh-themes repository, which
|
|
// mounts the themes conveniently by name to the module object, and which
|
|
// is embedded locally inside the HackMyResume installation.
|
|
// TODO: merge this code with
|
|
let themePath;
|
|
const themesObj = require('fresh-themes');
|
|
if (_.has(themesObj.themes, th)) {
|
|
themePath = PATH.join(
|
|
parsePath( require.resolve('fresh-themes') ).dirname,
|
|
'/themes/',
|
|
th
|
|
);
|
|
} else {
|
|
const d = parsePath( th ).dirname;
|
|
themePath = PATH.join(d, th);
|
|
}
|
|
|
|
cached[ th ] = cached[th] || new FRESHTheme().open( themePath );
|
|
return formatsHash[ key ] = cached[ th ].getFormat( key );
|
|
});
|
|
}
|
|
|
|
// Load theme files
|
|
formatsHash = _load.call(this, formatsHash);
|
|
|
|
// Cache
|
|
this.formats = formatsHash;
|
|
|
|
// Set the official theme name
|
|
this.name = parsePath( this.folder ).name;
|
|
return this;
|
|
}
|
|
|
|
/* Determine if the theme supports the specified output format. */
|
|
hasFormat( fmt ) { return _.has(this.formats, fmt); }
|
|
|
|
/* Determine if the theme supports the specified output format. */
|
|
getFormat( fmt ) { return this.formats[ fmt ]; }
|
|
}
|
|
|
|
|
|
|
|
/* Load and parse theme source files. */
|
|
var _load = function(formatsHash) {
|
|
|
|
const that = this;
|
|
const major = false;
|
|
const tplFolder = PATH.join(this.folder, this.baseFolder);
|
|
|
|
const copyOnly = ['.ttf','.otf', '.png','.jpg','.jpeg','.pdf'];
|
|
|
|
// Iterate over all files in the theme folder, producing an array, fmts,
|
|
// containing info for each file. While we're doing that, also build up
|
|
// the formatsHash object.
|
|
const fmts = READFILES(tplFolder).map(function(absPath) {
|
|
return _loadOne.call(this, absPath, formatsHash, tplFolder);
|
|
}
|
|
, this);
|
|
|
|
// Now, get all the CSS files...
|
|
this.cssFiles = fmts.filter(fmt => fmt && (fmt.ext === 'css'));
|
|
|
|
// For each CSS file, get its corresponding HTML file. It's possible that
|
|
// a theme can have a CSS file but *no* HTML file, as when a theme author
|
|
// creates a pure CSS override of an existing theme.
|
|
this.cssFiles.forEach(function(cssf) {
|
|
const idx = _.findIndex(fmts, fmt => fmt && (fmt.pre === cssf.pre) && (fmt.ext === 'html'));
|
|
cssf.major = false;
|
|
if (idx > -1) {
|
|
fmts[ idx ].css = cssf.data;
|
|
return fmts[ idx ].cssPath = cssf.path;
|
|
} else {
|
|
if (that.inherits) {
|
|
// Found a CSS file without an HTML file in a theme that inherits
|
|
// from another theme. This is the override CSS file.
|
|
return that.overrides = { file: cssf.path, data: cssf.data };
|
|
}
|
|
}});
|
|
|
|
// Now, save all the javascript file paths to a theme property.
|
|
const jsFiles = fmts.filter(fmt => fmt && (fmt.ext === 'js'));
|
|
this.jsFiles = jsFiles.map(jsf => jsf['path']);
|
|
|
|
return formatsHash;
|
|
};
|
|
|
|
|
|
/* Load a single theme file. */
|
|
var _loadOne = function( absPath, formatsHash, tplFolder ) {
|
|
|
|
const pathInfo = parsePath(absPath);
|
|
if (pathInfo.basename.toLowerCase() === 'theme.json') { return; }
|
|
|
|
const absPathSafe = absPath.trim().toLowerCase();
|
|
let outFmt = '';
|
|
let act = 'copy';
|
|
let isPrimary = false;
|
|
|
|
// If this is an "explicit" theme, all files of importance are specified in
|
|
// the "transform" section of the theme.json file.
|
|
if (this.explicit) {
|
|
|
|
outFmt = _.find(Object.keys( this.formats ), function( fmtKey ) {
|
|
const fmtVal = this.formats[ fmtKey ];
|
|
return _.some(fmtVal.transform, function(fpath) {
|
|
const absPathB = PATH.join( this.folder, fpath ).trim().toLowerCase();
|
|
return absPathB === absPathSafe;
|
|
}
|
|
, this);
|
|
}
|
|
, this);
|
|
if (outFmt) { act = 'transform'; }
|
|
}
|
|
|
|
if (!outFmt) {
|
|
// If this file lives in a specific format folder within the theme,
|
|
// such as "/latex" or "/html", then that format is the implicit output
|
|
// format for all files within the folder
|
|
const portion = pathInfo.dirname.replace(tplFolder,'');
|
|
if (portion && portion.trim()) {
|
|
if (portion[1] === '_') { return; }
|
|
const reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
|
|
const res = reg.exec( portion );
|
|
if (res) {
|
|
if (res[1] !== 'partials') {
|
|
outFmt = res[1];
|
|
if (!this.explicit) { act = 'transform'; }
|
|
} else {
|
|
this.partials = this.partials || [];
|
|
this.partials.push( { name: pathInfo.name, path: absPath } );
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Otherwise, the output format is inferred from the filename, as in
|
|
// compact-[outputformat].[extension], for ex, compact-pdf.html
|
|
if (!outFmt) {
|
|
const idx = pathInfo.name.lastIndexOf('-');
|
|
outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx+1);
|
|
if (!this.explicit) { act = 'transform'; }
|
|
const defFormats = require('./default-formats');
|
|
isPrimary = _.some(defFormats, form => (form.name === outFmt) && (pathInfo.extname !== '.css'));
|
|
}
|
|
|
|
// Make sure we have a valid formatsHash
|
|
formatsHash[ outFmt ] = formatsHash[outFmt] || {
|
|
outFormat: outFmt,
|
|
files: []
|
|
};
|
|
|
|
// Move symlink descriptions from theme.json to the format
|
|
if (__guard__(this.formats != null ? this.formats[outFmt ] : undefined, x => x.symLinks)) {
|
|
formatsHash[ outFmt ].symLinks = this.formats[ outFmt ].symLinks;
|
|
}
|
|
|
|
// Create the file representation object
|
|
const obj = {
|
|
action: act,
|
|
primary: isPrimary,
|
|
path: absPath,
|
|
orgPath: PATH.relative(tplFolder, absPath),
|
|
ext: pathInfo.extname.slice(1),
|
|
title: friendlyName(outFmt),
|
|
pre: outFmt,
|
|
// outFormat: outFmt || pathInfo.name,
|
|
data: FS.readFileSync(absPath, 'utf8'),
|
|
css: null
|
|
};
|
|
|
|
// Add this file to the list of files for this format type.
|
|
formatsHash[ outFmt ].files.push( obj );
|
|
return obj;
|
|
};
|
|
|
|
|
|
|
|
/* Return a more friendly name for certain formats. */
|
|
var friendlyName = function( val ) {
|
|
val = (val && val.trim().toLowerCase()) || '';
|
|
const friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
|
|
return friendly[val] || val;
|
|
};
|
|
|
|
|
|
|
|
module.exports = FRESHTheme;
|
|
|
|
function __guard__(value, transform) {
|
|
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
|
|
} |