peregrine-cms-test-html
Version:
Test your peregrine site to see what is supported.
443 lines (415 loc) • 12.9 kB
JavaScript
;
/**
* Tests all sites' text input fields for Peregrine unsupported content.
*
* Check logs folder to see detailed analysis of text inputs.
*
* @file This file is run with the cmd 'npm run testHTML [sites...]'.
* @author Shane Mcgrath
* @since 7.31.2020
*/
const fs = require("fs");
const path = require("path");
const util = require("util");
const parseStringPromise = require("xml2js").parseStringPromise;
const cheerio = require("cheerio");
const fileIterator = require("./fileIterator");
process.argv.shift();
process.argv.shift();
if (!fs.existsSync(path.join(path.resolve('./'), "/logs"))) {
fs.mkdirSync(path.join(path.resolve('./'), "/logs"));
}
/**
* Gets all folders underneath the parent folder passed in.
*
* @param {string} parentPath - ParentPath of directory.
*
* @example
* // returns ['apple', 'apachepeakconcepts', 'golfdigest', 'headwire', 'sap', 'spicy_mango']
* getFolders('Users/shane/samplesites');
* @returns {Array} Returns a list of names of sub folders.
*/
const getFolders = (parentPath) => {
return fs
.readdirSync(parentPath)
.filter((folder) =>
fs.lstatSync(path.join(parentPath, folder)).isDirectory()
);
};
const supported = {
a: ["style", "title", "href", "target"],
img: ["style", "alt", "title", "src"],
p: ["style"],
h1: ["style"],
h2: ["style"],
h3: ["style"],
h4: ["style"],
h5: ["style"],
h6: ["style"],
li: ["style"],
div: ["style"],
br: ["style"],
ul: ["style"],
ol: ["style"],
b: ["style"],
i: ["style"],
sub: ["style"],
sup: ["style"],
};
process.argv =
process.argv.length > 0
? process.argv
: getFolders(path.resolve('./')).filter((subfolder) => {
if (
getFolders(path.join(path.resolve('./'), subfolder)).indexOf("jcr_root") > 0
) {
return true;
}
});
searchSites(process.argv);
/**
* Loops over sites.
*
* Ceates a log file for each site and finds the files from the head folders for each site.
* Prints ending user message when search is complete.
*
* @param {Array} sites - List of sites indicated by the user.
*
* @example
* searchSites(['apple', 'apachepeakconcepts', 'golfdigest', 'headwire', 'sap', 'spicy_mango']);
*/
function searchSites(sites) {
sites.forEach((site) => {
fs.existsSync(path.join(path.resolve('./'), "logs", `${site}.log`))
? fs.truncateSync(path.join(path.resolve('./'), "logs", `${site}.log`))
: fs.writeFileSync(path.join(path.resolve('./'), "logs", `${site}.log`), "");
const log_file = fs.createWriteStream(
path.join(path.resolve('./'), "logs", `${site}.log`),
{
flags: "w",
}
);
const parentMainFolder = path.join(
path.resolve('./'),
site,
"jcr_root/content",
);
let siteObject = { site, log_file };
findFiles(parentMainFolder, siteObject).then(() => {
endingMessage(siteObject);
});
});
}
/**
* Finds all content files from directory passed in.
*
* Parses content.xml files to a json object.
* Checks to see if the file belongs to a page or template.
* Traverses over json object.
* Calls validateHTMLText and passes in text returned from tranverse generator.
*
* @param {string} dir - Directory that want to search.
* @param {object.<string, UnsupportedContent>} siteObject - Contains information of all the unsupported content.
* @param {string} siteObject.site - Site that is currently being traversed.
* @param {Function} siteObject.log_file - Creates write stream to current site's log.
*
* @example
* findFiles(
* 'Users/shane/samplesites/apple/jcr_root/apple/pages',
* {
* site: 'apple',
* log_file: createWriteStream('Users/shane/samplesites/logs/apple.log')
* }
* );
*/
async function findFiles(dir, siteObject) {
for await (const file of fileIterator(dir)) {
if (
path.basename(file) === ".content.xml" &&
file.indexOf("pages/library") === -1 &&
file.indexOf("examplepage/library") === -1
) {
try {
const json = await parseStringPromise(await fs.promises.readFile(file));
if (json["jcr:root"]['$']['jcr:primaryType'] !== "per:Page") continue;
for (const nodeObj of traverse(json["jcr:root"]["jcr:content"][0])) {
validateHTMLText(
nodeObj.text,
nodeObj.resourceType,
file,
siteObject
);
}
} catch (err) {
console.log("Error at: ", file);
console.log(err);
}
}
}
}
/**
* Traverses component/node tree.
*
* @typedef Traverse
*
* @param {object} node - Node that is currently being traversed.
* @param {object} node.$ - Current component/node.
*
* @yields {object} - Yields an object with each property within the current component along with the resourceType of the node if available.
* @yields {Traverse} - Recursively traverses through current node's children.
*
* @example
* traverse(
* {
* "$": {
* "sling:resourceType": "apple/components/container"
* },
* child: [
* {
* "$": {
* "sling:resourceType": "apple/components/richtext",
* "text": "<h1>Here at apple we give our best to make the customer happy.</h1>",
* }
* }
* ]
* }
* );
*/
function* traverse(node) {
for (let prop in node["$"]) {
yield {
text: node["$"][prop],
resourceType: node["$"]["sling:resourceType"]
? node["$"]["sling:resourceType"]
: "",
};
}
for (const child in node) {
if (child != "$") yield* traverse(node[child][0]);
}
}
/**
* Checks line of text that is passed in and tests for valid HTML supported by Peregrine.
*
* Loops over every element in html string and checks its tag, styles within the style attribute, and all other attributes not supported in Peregrine's current state.
*
* @param {string} text - Text being validated.
* @param {string} resourceType - ResourceType of component where text is found.
* @param {string} file - File where text is found.
* @param {object.<string, UnsupportedContent>} siteObject - Contains information of all the unsupported content.
* @param {string} siteObject.site - Site that is currently being traversed.
* @param {Function} siteObject.log_file - Write stream to current site's log.
*
* @example
* validateHTMLText(
* "<h1>Here at apple we give our best to make the customer happy.</h1>",
* "apple/components/richtext",
* "Users/shane/samplesites/apple/jcr_content/apple/pages/index",
* {
* site: 'apple',
* log_file: createWriteStream('Users/shane/samplesites/logs/apple.log')
* }
* );
*/
function validateHTMLText(text, resourceType, file, siteObject) {
const $ = cheerio.load(text);
let unsupportedTests = {
style: {
count: 0,
type: "attribute",
},
};
$("*").each((index, element) => {
checkStyles($(element), unsupportedTests);
checkAttributes($(element), unsupportedTests);
checkTags($(element), unsupportedTests);
});
for (let test in unsupportedTests) {
if (unsupportedTests[test].count > 0) {
if (!siteObject[test]) {
siteObject[test] = new UnsupportedContent(
test,
unsupportedTests[test].type
);
siteObject[test].setCount(unsupportedTests[test].count);
} else {
siteObject[test].increment(unsupportedTests[test].count);
}
siteObject.log_file.write(
util.format(
`Unsupported use of ${
siteObject[test].contentString
} on text \n ${text}${
resourceType ? `\n in ${resourceType}` : ""
} \n at ${file}\n\n`
)
);
}
}
}
/**
* Checks inline styles of element and if not supported, is added to the unsupported tests.
*
* @param {object} element - JQuery supported element.
* @param {object.<string, {count: number, type: string}>} unsupportedTests - Contains information of all the unsupported content.
*
* @example
* checkStyles(
* "<h1>Here at apple we give our best to make the customer happy.</h1>",
* {
* styles: {
* count: 0,
* type: "attribute"
* },
* "text-decoration": {
* count: 1,
* type: "style"
* }
* }
* );
*/
function checkStyles(element, unsupportedTests) {
element.css("text-align", "");
if (element.attr("style")) {
unsupportedTests.style.count++;
for (let style in element.css()) {
if (unsupportedTests[style]) {
unsupportedTests[style].count++;
} else if (!unsupportedTests[style]) {
unsupportedTests[style] = {
count: 1,
type: "style",
};
}
}
}
}
/**
* Checks attributes of element and if not supported, is added to the unsupported tests.
*
* @param {object} element - JQuery supported element.
* @param {object.<string, {count: number, type: string}>} unsupportedTests - Contains information of all the unsupported content.
*
* @example
* checkAttributes(
* "<h1>Here at apple we give our best to make the customer happy.</h1>",
* {
* styles: {
* count: 0,
* type: "attribute"
* },
* "text-decoration": {
* count: 1,
* type: "style"
* }
* }
* );
*/
function checkAttributes(element, unsupportedTests) {
let name = element.prop("tagName").toLowerCase();
for (let attribute in element.attr()) {
if (supported[name] && supported[name].indexOf(attribute) > -1) continue;
if (unsupportedTests[attribute]) {
unsupportedTests[attribute].count++;
} else if (!unsupportedTests[attribute]) {
unsupportedTests[attribute] = {
count: 1,
type: "attribute",
};
}
}
}
/**
* Checks the tag name of the element and if not supported, is added to the unsupported tests.
*
* @param {object} element - JQuery supported element.
* @param {object.<string, {count: number, type: string}>} unsupportedTests - Contains information of all the unsupported content.
*
* @example
* checkTags(
* "<h1>Here at apple we give our best to make the customer happy.</h1>",
* {
* styles: {
* count: 0,
* type: "attribute"
* },
* "text-decoration": {
* count: 1,
* type: "style"
* }
* }
* );
*/
function checkTags(element, unsupportedTests) {
let name = element.prop("tagName").toLowerCase();
let isSupportedTag = false;
if (supported[name]) {
isSupportedTag = true;
}
if (!isSupportedTag && unsupportedTests[name]) {
unsupportedTests[name].count++;
} else if (!isSupportedTag && !unsupportedTests[name]) {
unsupportedTests[name] = {
count: 1,
type: "tag",
};
}
}
/**
* Prints message to user of all different types of unsupported content and number of times seen in site indicated.
*
* @param {object.<string, UnsupportedContent>} siteObject - Contains information of all the unsupported content.
* @param {string} siteObject.site - Site that is currently being traversed.
* @param {Function} siteObject.log_file - Write stream to current site's log.
*
* @example
* endingMessage(
* {
* site: 'apple',
* log_file: createWriteStream('Users/shane/samplesites/logs/apple.log'),
* }
* );
*/
function endingMessage(siteObject) {
for (let test in siteObject) {
if (test !== "site" && test !== "log_file")
console.log(
`Unsupported use of ${siteObject[test].contentString} in ${siteObject.site}\n`
);
}
siteObject.log_file.end();
console.log(
`Can find more detailed information in ${path.resolve('./')}/logs/${siteObject.site}.log\n`
);
}
/**
* Data structure to keep track of unsupported content.
*
* @param {string} content - Unique unsupported error.
* @param {string} type - ( tag, attribute, style ).
*
* @property {number} count - Total number of instances of this content error.
* @property {string} content - Unique unsupported error.
* @property {string} type - ( tag, attribute, style ).
* @property {string} contentString - (the type content: {the content}({number of times content seen in site})).
*/
class UnsupportedContent {
constructor(content, type) {
this.count = 0;
this.content = content;
this.type = type;
this.contentString = "";
}
setContentString() {
this.contentString = `${this.type}: ${this.content}(${this.count})`;
}
setCount(initValue = 1) {
this.count = initValue;
this.setContentString();
}
increment(initValue = 1) {
this.count += initValue;
this.setContentString();
}
}