UNPKG

peregrine-cms-test-html

Version:

Test your peregrine site to see what is supported.

443 lines (415 loc) 12.9 kB
#!/usr/bin/env node 'use strict'; /** * 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(); } }