patternlab-node
Version:
Pattern Lab is a collection of tools to help you create atomic design systems. This is the node command line interface (CLI).
616 lines (513 loc) • 24.1 kB
JavaScript
;
var path = require('path'),
_ = require('lodash'),
fs = require('fs-extra'),
Pattern = require('./object_factory').Pattern,
CompileState = require('./object_factory').CompileState,
pph = require('./pseudopattern_hunter'),
mp = require('./markdown_parser'),
plutils = require('./utilities'),
dataLoader = require('./data_loader')(),
patternEngines = require('./pattern_engines'),
lh = require('./lineage_hunter'),
lih = require('./list_item_hunter'),
smh = require('./style_modifier_hunter'),
ph = require('./parameter_hunter'),
jsonCopy = require('./json_copy'),
ch = require('./changes_hunter');
const markdown_parser = new mp();
const changes_hunter = new ch();
var pattern_assembler = function () {
// HELPER FUNCTIONS
function getPartial(partialName, patternlab) {
//look for exact partial matches
for (var i = 0; i < patternlab.patterns.length; i++) {
if (patternlab.patterns[i].patternPartial === partialName) {
return patternlab.patterns[i];
}
}
//else look by verbose syntax
for (var i = 0; i < patternlab.patterns.length; i++) {
switch (partialName) {
case patternlab.patterns[i].relPath:
case patternlab.patterns[i].verbosePartial:
return patternlab.patterns[i];
}
}
//return the fuzzy match if all else fails
for (var i = 0; i < patternlab.patterns.length; i++) {
var partialParts = partialName.split('-'),
partialType = partialParts[0],
partialNameEnd = partialParts.slice(1).join('-');
if (patternlab.patterns[i].patternPartial.split('-')[0] === partialType && patternlab.patterns[i].patternPartial.indexOf(partialNameEnd) > -1) {
return patternlab.patterns[i];
}
}
plutils.warning('Could not find pattern referenced with partial syntax ' + partialName + '. This can occur when a pattern was renamed, moved, or no longer exists but it still called within a different template somewhere.');
return undefined;
}
function buildListItems(container) {
//combine all list items into one structure
var list = [];
for (var item in container.listitems) {
if (container.listitems.hasOwnProperty(item)) {
list.push(container.listitems[item]);
}
}
container.listItemArray = plutils.shuffle(list);
for (var i = 1; i <= container.listItemArray.length; i++) {
var tempItems = [];
if (i === 1) {
tempItems.push(container.listItemArray[0]);
container.listitems['' + i ] = tempItems;
} else {
for (var c = 1; c <= i; c++) {
tempItems.push(container.listItemArray[c - 1]);
container.listitems['' + i ] = tempItems;
}
}
}
}
/*
* Deprecated in favor of .md 'status' frontmatter inside a pattern. Still used for unit tests at this time.
* Will be removed in future versions
*/
function setState(pattern, patternlab, displayDeprecatedWarning) {
if (patternlab.config.patternStates && patternlab.config.patternStates[pattern.patternPartial]) {
if (displayDeprecatedWarning) {
plutils.error("Deprecation Warning: Using patternlab-config.json patternStates object will be deprecated in favor of the state frontmatter key associated with individual pattern markdown files.");
console.log("This feature will still work in it's current form this release (but still be overridden by the new parsing method), and will be removed in the future.");
}
pattern.patternState = patternlab.config.patternStates[pattern.patternPartial];
}
}
function addPattern(pattern, patternlab) {
//add the link to the global object
patternlab.data.link[pattern.patternPartial] = '/patterns/' + pattern.patternLink;
//only push to array if the array doesn't contain this pattern
var isNew = true;
for (var i = 0; i < patternlab.patterns.length; i++) {
//so we need the identifier to be unique, which patterns[i].relPath is
if (pattern.relPath === patternlab.patterns[i].relPath) {
//if relPath already exists, overwrite that element
patternlab.patterns[i] = pattern;
patternlab.partials[pattern.patternPartial] = pattern.extendedTemplate || pattern.template;
isNew = false;
break;
}
}
// if the pattern is new, we must register it with various data structures!
if (isNew) {
if (patternlab.config.debug) {
console.log('found new pattern ' + pattern.patternPartial);
}
// do global registration
if (pattern.isPattern) {
patternlab.partials[pattern.patternPartial] = pattern.extendedTemplate || pattern.template;
// do plugin-specific registration
pattern.registerPartial();
} else {
patternlab.partials[pattern.patternPartial] = pattern.patternDesc;
}
patternlab.graph.add(pattern);
patternlab.patterns.push(pattern);
}
}
function addSubtypePattern(subtypePattern, patternlab) {
patternlab.subtypePatterns[subtypePattern.patternPartial] = subtypePattern;
}
// Render a pattern on request. Long-term, this should probably go away.
function renderPattern(pattern, data, partials) {
// if we've been passed a full Pattern, it knows what kind of template it
// is, and how to render itself, so we just call its render method
if (pattern instanceof Pattern) {
return pattern.render(data, partials);
} else {
// otherwise, assume it's a plain mustache template string, and we
// therefore just need to create a dummpy pattern to be able to render
// it
var dummyPattern = Pattern.createEmpty({extendedTemplate: pattern});
return patternEngines.mustache.renderPattern(dummyPattern, data, partials);
}
}
function parsePatternMarkdown(currentPattern, patternlab) {
try {
var markdownFileName = path.resolve(patternlab.config.paths.source.patterns, currentPattern.subdir, currentPattern.fileName + ".md");
changes_hunter.checkLastModified(currentPattern, markdownFileName);
var markdownFileContents = fs.readFileSync(markdownFileName, 'utf8');
var markdownObject = markdown_parser.parse(markdownFileContents);
if (!plutils.isObjectEmpty(markdownObject)) {
//set keys and markdown itself
currentPattern.patternDescExists = true;
currentPattern.patternDesc = markdownObject.markdown;
//Add all markdown to the currentPattern, including frontmatter
currentPattern.allMarkdown = markdownObject;
//consider looping through all keys eventually. would need to blacklist some properties and whitelist others
if (markdownObject.state) {
currentPattern.patternState = markdownObject.state;
}
if (markdownObject.order) {
currentPattern.order = markdownObject.order;
}
if (markdownObject.hidden) {
currentPattern.hidden = markdownObject.hidden;
}
if (markdownObject.excludeFromStyleguide) {
currentPattern.excludeFromStyleguide = markdownObject.excludeFromStyleguide;
}
if (markdownObject.tags) {
currentPattern.tags = markdownObject.tags;
}
if (markdownObject.title) {
currentPattern.patternName = markdownObject.title;
}
if (markdownObject.links) {
currentPattern.links = markdownObject.links;
}
} else {
if (patternlab.config.debug) {
console.log('error processing markdown for ' + currentPattern.patternPartial);
}
}
if (patternlab.config.debug) {
console.log('found pattern-specific markdown for ' + currentPattern.patternPartial);
}
}
catch (err) {
// do nothing when file not found
if (err.code !== 'ENOENT') {
console.log('there was an error setting pattern keys after markdown parsing of the companion file for pattern ' + currentPattern.patternPartial);
console.log(err);
}
}
}
/**
* A helper that unravels a pattern looking for partials or listitems to unravel.
* The goal is really to convert pattern.template into pattern.extendedTemplate
* @param pattern - the pattern to decompose
* @param patternlab - global data store
* @param ignoreLineage - whether or not to hunt for lineage for this pattern
*/
function decomposePattern(pattern, patternlab, ignoreLineage) {
var lineage_hunter = new lh(),
list_item_hunter = new lih();
pattern.extendedTemplate = pattern.template;
//find how many partials there may be for the given pattern
var foundPatternPartials = pattern.findPartials();
//find any listItem blocks that within the pattern, even if there are no partials
list_item_hunter.process_list_item_partials(pattern, patternlab);
// expand any partials present in this pattern; that is, drill down into
// the template and replace their calls in this template with rendered
// results
if (pattern.engine.expandPartials && (foundPatternPartials !== null && foundPatternPartials.length > 0)) {
// eslint-disable-next-line
expandPartials(foundPatternPartials, list_item_hunter, patternlab, pattern);
// update the extendedTemplate in the partials object in case this
// pattern is consumed later
patternlab.partials[pattern.patternPartial] = pattern.extendedTemplate;
}
//find pattern lineage
if (!ignoreLineage) {
lineage_hunter.find_lineage(pattern, patternlab);
}
//add to patternlab object so we can look these up later.
addPattern(pattern, patternlab);
}
function processPatternIterative(relPath, patternlab) {
var relativeDepth = (relPath.match(/\w(?=\\)|\w(?=\/)/g) || []).length;
if (relativeDepth > 2) {
console.log('');
plutils.warning('Warning:');
plutils.warning('A pattern file: ' + relPath + ' was found greater than 2 levels deep from ' + patternlab.config.paths.source.patterns + '.');
plutils.warning('It\'s strongly suggested to not deviate from the following structure under _patterns/');
plutils.warning('[patternType]/[patternSubtype]/[patternName].[patternExtension]');
console.log('');
plutils.warning('While Pattern Lab may still function, assets may 404 and frontend links may break. Consider yourself warned. ');
plutils.warning('Read More: http://patternlab.io/docs/pattern-organization.html');
console.log('');
}
//check if the found file is a top-level markdown file
var fileObject = path.parse(relPath);
if (fileObject.ext === '.md') {
try {
var proposedDirectory = path.resolve(patternlab.config.paths.source.patterns, fileObject.dir, fileObject.name);
var proposedDirectoryStats = fs.statSync(proposedDirectory);
if (proposedDirectoryStats.isDirectory()) {
var subTypeMarkdownFileContents = fs.readFileSync(proposedDirectory + '.md', 'utf8');
var subTypeMarkdown = markdown_parser.parse(subTypeMarkdownFileContents);
var subTypePattern = new Pattern(relPath, null, patternlab);
subTypePattern.patternSectionSubtype = true;
subTypePattern.patternLink = subTypePattern.name + '/index.html';
subTypePattern.patternDesc = subTypeMarkdown.markdown;
subTypePattern.flatPatternPath = subTypePattern.flatPatternPath + '-' + subTypePattern.fileName;
subTypePattern.isPattern = false;
subTypePattern.engine = null;
addSubtypePattern(subTypePattern, patternlab);
return subTypePattern;
}
} catch (err) {
// no file exists, meaning it's a pattern markdown file
if (err.code !== 'ENOENT') {
console.log(err);
}
}
}
var pseudopattern_hunter = new pph();
//extract some information
var filename = fileObject.base;
var ext = fileObject.ext;
var patternsPath = patternlab.config.paths.source.patterns;
// skip non-pattern files
if (!patternEngines.isPatternFile(filename, patternlab)) { return null; }
//make a new Pattern Object
var currentPattern = new Pattern(relPath, null, patternlab);
//if file is named in the syntax for variants
if (patternEngines.isPseudoPatternJSON(filename)) {
return currentPattern;
}
//can ignore all non-supported files at this point
if (patternEngines.isFileExtensionSupported(ext) === false) {
return currentPattern;
}
//see if this file has a state
setState(currentPattern, patternlab, true);
//look for a json file for this template
try {
var jsonFilename = path.resolve(patternsPath, currentPattern.subdir, currentPattern.fileName);
let configData = dataLoader.loadDataFromFile(jsonFilename, fs);
if (configData) {
currentPattern.jsonFileData = configData;
if (patternlab.config.debug) {
console.log('processPatternIterative: found pattern-specific config data for ' + currentPattern.patternPartial);
}
}
}
catch (err) {
console.log('There was an error parsing sibling JSON for ' + currentPattern.relPath);
console.log(err);
}
//look for a listitems.json file for this template
try {
var listJsonFileName = path.resolve(patternsPath, currentPattern.subdir, currentPattern.fileName + ".listitems");
let listItemsConfig = dataLoader.loadDataFromFile(listJsonFileName, fs);
if (listItemsConfig) {
currentPattern.listitems = listItemsConfig;
buildListItems(currentPattern);
if (patternlab.config.debug) {
console.log('found pattern-specific listitems config for ' + currentPattern.patternPartial);
}
}
}
catch (err) {
console.log('There was an error parsing sibling listitem JSON for ' + currentPattern.relPath);
console.log(err);
}
//look for a markdown file for this template
parsePatternMarkdown(currentPattern, patternlab);
//add the raw template to memory
var templatePath = path.resolve(patternsPath, currentPattern.relPath);
currentPattern.template = fs.readFileSync(templatePath, 'utf8');
//find any stylemodifiers that may be in the current pattern
currentPattern.stylePartials = currentPattern.findPartialsWithStyleModifiers();
//find any pattern parameters that may be in the current pattern
currentPattern.parameteredPartials = currentPattern.findPartialsWithPatternParameters();
[templatePath, jsonFilename, listJsonFileName].forEach(file => {
changes_hunter.checkLastModified(currentPattern, file);
});
changes_hunter.checkBuildState(currentPattern, patternlab);
//add currentPattern to patternlab.patterns array
addPattern(currentPattern, patternlab);
//look for a pseudo pattern by checking if there is a file containing same name, with ~ in it, ending in .json
pseudopattern_hunter.find_pseudopatterns(currentPattern, patternlab);
return currentPattern;
}
function processPatternRecursive(file, patternlab) {
//find current pattern in patternlab object using var file as a partial
var currentPattern, i;
for (i = 0; i < patternlab.patterns.length; i++) {
if (patternlab.patterns[i].relPath === file) {
currentPattern = patternlab.patterns[i];
}
}
//return if processing an ignored file
if (typeof currentPattern === 'undefined') { return; }
//we are processing a markdown only pattern
if (currentPattern.engine === null) { return; }
//call our helper method to actually unravel the pattern with any partials
decomposePattern(currentPattern, patternlab);
}
/**
* Finds patterns that were modified and need to be rebuilt. For clean patterns load the already
* rendered markup.
*
* @param lastModified
* @param patternlab
*/
function markModifiedPatterns(lastModified, patternlab) {
/**
* If the given array exists, apply a function to each of its elements
* @param {Array} array
* @param {Function} func
*/
const forEachExisting = (array, func) => {
if (array) {
array.forEach(func);
}
};
const modifiedOrNot = _.groupBy(
patternlab.patterns,
p => changes_hunter.needsRebuild(lastModified, p) ? 'modified' : 'notModified');
// For all unmodified patterns load their rendered template output
forEachExisting(modifiedOrNot.notModified, cleanPattern => {
const xp = path.join(patternlab.config.paths.public.patterns, cleanPattern.getPatternLink(patternlab, 'markupOnly'));
// Pattern with non-existing markupOnly files were already marked for rebuild and thus are not "CLEAN"
cleanPattern.patternPartialCode = fs.readFileSync(xp, 'utf8');
});
// For all patterns that were modified, schedule them for rebuild
forEachExisting(modifiedOrNot.modified, p => p.compileState = CompileState.NEEDS_REBUILD);
return modifiedOrNot;
}
function expandPartials(foundPatternPartials, list_item_hunter, patternlab, currentPattern) {
var style_modifier_hunter = new smh(),
parameter_hunter = new ph();
if (patternlab.config.debug) {
console.log('found partials for ' + currentPattern.patternPartial);
}
// determine if the template contains any pattern parameters. if so they
// must be immediately consumed
parameter_hunter.find_parameters(currentPattern, patternlab);
//do something with the regular old partials
for (var i = 0; i < foundPatternPartials.length; i++) {
var partial = currentPattern.findPartial(foundPatternPartials[i]);
var partialPath;
//identify which pattern this partial corresponds to
for (var j = 0; j < patternlab.patterns.length; j++) {
if (patternlab.patterns[j].patternPartial === partial ||
patternlab.patterns[j].relPath.indexOf(partial) > -1)
{
partialPath = patternlab.patterns[j].relPath;
}
}
//recurse through nested partials to fill out this extended template.
processPatternRecursive(partialPath, patternlab);
//complete assembly of extended template
//create a copy of the partial so as to not pollute it after the getPartial call.
var partialPattern = getPartial(partial, patternlab);
var cleanPartialPattern = jsonCopy(partialPattern, `partial pattern ${partial}`);
//if partial has style modifier data, replace the styleModifier value
if (currentPattern.stylePartials && currentPattern.stylePartials.length > 0) {
style_modifier_hunter.consume_style_modifier(cleanPartialPattern, foundPatternPartials[i], patternlab);
}
currentPattern.extendedTemplate = currentPattern.extendedTemplate.replace(foundPatternPartials[i], cleanPartialPattern.extendedTemplate);
}
}
function parseDataLinksHelper(patternlab, obj, key) {
var linkRE, dataObjAsString, linkMatches;
//check for 'link.patternPartial'
linkRE = /(?:'|")(link\.[A-z0-9-_]+)(?:'|")/g;
//stringify the passed in object
dataObjAsString = JSON.stringify(obj);
if (!dataObjAsString) { return obj; }
//find matches
linkMatches = dataObjAsString.match(linkRE);
if (linkMatches) {
for (var i = 0; i < linkMatches.length; i++) {
var dataLink = linkMatches[i];
if (dataLink && dataLink.split('.').length >= 2) {
//get the partial the link refers to
var linkPatternPartial = dataLink.split('.')[1].replace('"', '').replace("'", "");
var pattern = getPartial(linkPatternPartial, patternlab);
if (pattern !== undefined) {
//get the full built link and replace it
var fullLink = patternlab.data.link[linkPatternPartial];
if (fullLink) {
fullLink = path.normalize(fullLink).replace(/\\/g, '/');
if (patternlab.config.debug) {
console.log('expanded data link from ' + dataLink + ' to ' + fullLink + ' inside ' + key);
}
//also make sure our global replace didn't mess up a protocol
fullLink = fullLink.replace(/:\//g, '://');
dataObjAsString = dataObjAsString.replace('link.' + linkPatternPartial, fullLink);
}
} else {
if (patternlab.config.debug) {
console.log('pattern not found for', dataLink, 'inside', key);
}
}
}
}
}
var dataObj;
try {
dataObj = JSON.parse(dataObjAsString);
} catch (err) {
console.log('There was an error parsing JSON for ' + key);
console.log(err);
}
return dataObj;
}
//look for pattern links included in data files.
//these will be in the form of link.* WITHOUT {{}}, which would still be there from direct pattern inclusion
function parseDataLinks(patternlab) {
//look for link.* such as link.pages-blog as a value
patternlab.data = parseDataLinksHelper(patternlab, patternlab.data, 'data.json');
//loop through all patterns
for (var i = 0; i < patternlab.patterns.length; i++) {
patternlab.patterns[i].jsonFileData = parseDataLinksHelper(patternlab, patternlab.patterns[i].jsonFileData, patternlab.patterns[i].patternPartial);
}
}
return {
mark_modified_patterns: function (lastModified, patternlab) {
return markModifiedPatterns(lastModified, patternlab);
},
find_pattern_partials: function (pattern) {
return pattern.findPartials();
},
find_pattern_partials_with_style_modifiers: function (pattern) {
return pattern.findPartialsWithStyleModifiers();
},
find_pattern_partials_with_parameters: function (pattern) {
return pattern.findPartialsWithPatternParameters();
},
find_list_items: function (pattern) {
return pattern.findListItems();
},
setPatternState: function (pattern, patternlab, displayDeprecatedWarning) {
setState(pattern, patternlab, displayDeprecatedWarning);
},
addPattern: function (pattern, patternlab) {
addPattern(pattern, patternlab);
},
addSubtypePattern: function (subtypePattern, patternlab) {
addSubtypePattern(subtypePattern, patternlab);
},
decomposePattern: function (pattern, patternlab, ignoreLineage) {
decomposePattern(pattern, patternlab, ignoreLineage);
},
renderPattern: function (template, data, partials) {
return renderPattern(template, data, partials);
},
process_pattern_iterative: function (file, patternlab) {
return processPatternIterative(file, patternlab);
},
process_pattern_recursive: function (file, patternlab, additionalData) {
processPatternRecursive(file, patternlab, additionalData);
},
getPartial: function (partial, patternlab) {
return getPartial(partial, patternlab);
},
combine_listItems: function (patternlab) {
buildListItems(patternlab);
},
parse_data_links: function (patternlab) {
parseDataLinks(patternlab);
},
parse_data_links_specific: function (patternlab, data, label) {
return parseDataLinksHelper(patternlab, data, label);
},
parse_pattern_markdown: function (pattern, patternlab) {
parsePatternMarkdown(pattern, patternlab);
}
};
};
module.exports = pattern_assembler;