UNPKG

@dwp/govuk-casa

Version:

Framework for creating basic GOVUK Collect-And-Submit-Applications

223 lines (205 loc) 6.83 kB
const fs = require('fs'); const path = require('path'); const nUrl = require('url'); const resolveObjPath = require('object-resolve-path'); const logger = require('./Logger')('util'); /** * Generate an object path string that we can use to traverse an object using * `objectPathValue()`. * * @param {...string} paths Path components. * @returns {string} Constructed path. */ function objectPathString(...paths) { let pathString = paths.length ? paths.join('.') : ''; // FIX: To support unquoted objects, wrap them in quotes // TASK: Contribute this back to vendor repo. pathString = pathString.replace(/\[([a-zA-Z_]+[a-zA-Z0-9_]*)\]/g, '["$1"]'); return pathString; } /** * Find the value at the given path of an object. For example, given the * object ... * { * rooms: { * lounge: { * objects: [ * 'TV' * ] * } * } * } * ... A call to `objectPathValue(obj, 'rooms.lounge.objects[0]')` would * return "TV". * * If using square-braces to access object keys, you also need to wrap the key * in quotes as you would in JavaScript for any strings that contain non * alphanumeric character, or begin with a number. * * Ref: * https://www.npmjs.com/package/object-resolve-path * * @param {object} obj Object to traverse * @param {string} paths Path component(s) to use * @returns {any} The value of the object, or `undefined` if not found */ function objectPathValue(obj, ...paths) { let pathString = objectPathString(...paths); // If the first path element contains non-valid path characters (^a-z0-9_) // then wrap it with square notation pathString = pathString.replace(/^([^[.]+)?($|\[|\.)+/i, '["$1"]$2'); try { return resolveObjPath(obj, pathString); } catch (e) { logger.debug('Object path is invalid: %s (%s)', pathString, e.message); return undefined; } } /** * Given an object path string generated by `objectPathString()`, create a * normalized version that can be used to refer to and name HTML form fields. * Specifically, this notation should use plain square braces for objects, with * no quotations. * * @param {string} pathString Path to normalize. * @returns {string} Normalized path. */ function normalizeHtmlObjectPath(pathString) { return pathString.replace(/\[["']([^\]]+?)['"]\]/g, '[$1]').replace(/[[\]]/g, '.').replace(/\.+/g, '.').replace(/\.+/g, '][') .replace(/\[+$/g, '') .replace(/^([^[\]]+)]/g, '$1') .replace(/\[([^\]]+)$/g, '[$1]') .replace(/\]+/g, ']') .replace(/\[+/g, '['); } /** * Determine if value is empty. Recurse over objects. * • Options: * RegExp regexRemove = characters matching this regex are removed before test * * @param {any} e Value to check * @param {object} options Options (see above) * @returns {boolean} True if the object is empty */ function isEmpty(e, options) { let val = e; if (typeof e === 'string' && options && options.regexRemove) { val = e.replace(options.regexRemove, ''); } if ( val === null || typeof val === 'undefined' || (typeof val === 'string' && val === '') ) { return true; } if (Array.isArray(val) || typeof val === 'object') { return Object.keys(val).filter((k) => !isEmpty(val[k], options)).length === 0; } return false; } /** * Determine which journey is associated with the "journey" part of the given * url. In multiple-journey mode, URLs are formatted as so: * /<journey-guid>/<waypoint-id> * * @param {Array} journeys Array of UserJourney.Map instances * @param {string} url URL to parse * @returns {UserJourney.Map|null} The associated journey */ function getJourneyFromUrl(journeys, url) { // Single-journeys are not reflected in URLs if (journeys.length === 1) { return journeys[0]; } const urlParts = url.replace(/^\/+/, '').split('/'); return urlParts.length ? (journeys.find((j) => (j.guid === urlParts[0])) || null) : null; } function getPageIdFromUrl(url) { return nUrl.parse(url).pathname.replace(/^\/+(.+)$/, '$1').replace(/\/+$/g, ''); } function getPageIdFromJourneyUrl(journey, url) { const strippedUrl = (journey && journey.guid) ? url.replace(new RegExp(`^/*${journey.guid}/*`), '/') : url; return getPageIdFromUrl(strippedUrl); } /** * Extract the originId, and current waypoint being visited from the given url. * * @param {string} url URL. * @returns {object} The origin ID, and current waypoint. */ function parseOriginWaypointInUrl(url) { try { const u = new URL(url, 'http://placeholder/'); const paths = u.pathname.match(/([^/]+)/g); let originId; let waypoint; if (paths) { [originId, waypoint] = paths.length > 1 ? paths : [undefined, paths[0]]; } return { originId, waypoint }; } catch (ex) { logger.error(ex.message); return { originId: undefined, waypoint: undefined, }; } } function isObjectType(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; } function isObjectWithKeys(target, keys = []) { const isObject = Object.prototype.toString.call(target) === '[object Object]'; let hasKeys = true; if (isObject) { keys.forEach((k) => { hasKeys = hasKeys && Object.prototype.hasOwnProperty.call(target, k); }); } return isObject && hasKeys; } /** * Discover the root folder of the specified npm module. * * @param {string} module Name of npm module to go and find. * @param {Array} paths Paths to search on for module folder. * @returns {string} The absolute path to the named module, if found. * @throws Error When the module cannot be found. * @throws SyntaxError When the module name contains invalid characters. */ function resolveModulePath(module = '', paths = []) { // Strip rogue chars from module name; only valid npm package names are // expected (https://docs.npmjs.com/files/package.json#name) const modName = module.replace(/[^a-z0-9\-_.]/ig, '').replace(/\.+/i, '.'); if (modName !== module) { throw new SyntaxError('Module name contains invalid characters'); } // Look for the module in the same places NodeJS would const resolved = paths.filter((p) => fs.existsSync(path.normalize(`${p}/${modName}`))); if (resolved.length) { return path.normalize(`${resolved[0]}/${modName}`); } throw new Error(`Cannot resolve module '${module}'`); } module.exports = { /** * Convert a given URL into an ID that we can use to uniquely identify a * specific page. This function will not verify the page ID is valid. * * @param {string} url Url to parse. * @returns {string} Page ID. */ getPageIdFromUrl, getPageIdFromJourneyUrl, getJourneyFromUrl, objectPathValue, objectPathString, normalizeHtmlObjectPath, isEmpty, isObjectType, isObjectWithKeys, resolveModulePath, parseOriginWaypointInUrl, };