UNPKG

@iebh/polyglot

Version:

IEBH-SRA tool to convert between different medical database search formats

403 lines (391 loc) 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _global = _interopRequireDefault(require("./global.js")); var _lodash = _interopRequireDefault(require("lodash")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } /** * Collection of utility functions to apply common behaviour to a compiled tree * @var {Object} */ var tools = { /** * Visit the given node types within a deeply nested tree and run a function * This function may mutate the input tree depending on the actions of the callbacks * NOTE: If the return value of the callback is `"DEL"` the node is deleted * @param {array} tree The tree sturcture to operate on * @param {null|array} types Node filter to apply to (if falsy all are used) * @param {function} callback The callback to call with each node. Called as (node, path) * @return {array} The input tree */ visit: function visit(tree, types, callback) { var removals = []; // Stack of removal paths we are performing when done var treeWalker = function treeWalker(tree, path) { tree.forEach(function (branch, branchKey) { var nodePath = path.concat(branchKey); // Fire callback if it matches if (!types || _lodash["default"].includes(types, branch.type)) { var result = callback(branch, nodePath); if (result === 'DEL') removals.push(nodePath); } // Walk down nodes if its a group if (branch.type == 'group' || branch.type == 'line') treeWalker(branch.nodes, nodePath.concat(['nodes'])); }); }; treeWalker(tree, []); // Crop all items marked as removals removals.reverse() // Walk in reverse order so we don't screw up arrays .forEach(function (path) { var nodeName = path.pop(); var parent = path.length ? _lodash["default"].get(tree, path) : tree; delete parent[nodeName]; }); return tree; }, /** * Apply a series of text replacements to every matching node object within a tree * This function mutates tree * @param {array} tree The tree sturcture to operate on * @param {null|array} types Type filter to apply. If falsy all are used * @param {array} replacements Array of replacements to apply. Each must be of the form `{subject: STRING|REGEXP, value: STRING|FUNCTION}` * @return {array} The input tree element with the replacements applied */ replaceContent: function replaceContent(tree, types, replacements) { this.visit(tree, types, function (branch) { if (!branch.content) return; replacements.forEach(function (replacement) { branch.content = branch.content.replace(replacement.subject, replacement.value); }); }); return tree; }, // Escape all regular expression chars except for pipe escapeRegExp: function escapeRegExp(string) { return string.replace(/[.*+?^${}()[\]\\]/g, '\\$&'); // $& means the whole matched string }, // Replace multiple terms at once multiReplace: function multiReplace(text, replaceObj) { var template = tools.escapeRegExp(Object.keys(replaceObj).join("|")); if (template.length > 0) { var regex = new RegExp(template, "g"); return text.replace(regex, function (match) { return replaceObj[match]; }); } else { return text; } }, /** * Retrieve the contents of a template by its ID * NOTE: If the specific engine definition is not found 'default' is used (and it will be pre-parsed via .translate()) * @param {string} template The template to resolve * @param {string} engine The current engine (used to get the correct sub-templating string) * @return {string} The resolved template */ resolveTemplate: function resolveTemplate(template, engine) { if (!_global["default"].templates[template]) return 'UNKNOWN-TEMPLATE:' + template; if (_global["default"].templates[template].engines[engine]) return _global["default"].templates[template].engines[engine]; if (_global["default"].templates[template].engines["default"]) return polyglot.translate(_global["default"].templates[template].engines["default"], engine); if (!_global["default"].templates[template].engines[engine]) return "Template: \"".concat(template, "\" not found for engine: \"").concat(engine, "\""); return ''; }, /** * Structure the wild cards correctly for cochrane to ensure no wildcards appear inside quotation marks * @param {string} text The text to parse * @param {Boolean} highlighting Whether to assign custom fonts * @return {string} The parsed string seperated by NEXT */ wildCardCochrane: function wildCardCochrane(text, highlighting) { var wildcards = ["?", "$", "*"]; var words = text.split(" "); var lastMatch = -1; var foundMatch = false; var _loop = function _loop(i) { if (wildcards.some(function (wildcard) { return words[i].includes(wildcard); })) { foundMatch = true; // Add quotation marks to previous word/s if the previous word was not a match if (i - 1 > lastMatch) { words[lastMatch + 1] = highlighting ? '<font color="DarkBlue">"' + words[lastMatch + 1] : '"' + words[lastMatch + 1]; words[i - 1] = highlighting ? words[i - 1] + '"</font>' : words[i - 1] + '"'; } lastMatch = i; // Check that there is a word before and it is not a wildcard word if (i > 0 && !wildcards.some(function (wildcard) { return words[i - 1].includes(wildcard); })) { words[i] = highlighting ? '<font color="purple">NEXT</font> ' + words[i] : 'NEXT ' + words[i]; } // Check that there is a word after if (i < words.length - 1) { words[i] = highlighting ? words[i] + ' <font color="purple">NEXT</font>' : words[i] + " NEXT"; } } }; for (var i = 0; i < words.length; i++) { _loop(i); } // Add quotation marks to word/s after the final match if (lastMatch + 1 < words.length) { words[lastMatch + 1] = highlighting ? '<font color="DarkBlue">"' + words[lastMatch + 1] : '"' + words[lastMatch + 1]; words[words.length - 1] = highlighting ? words[words.length - 1] + '"</font>' : words[words.length - 1] + '"'; } return foundMatch ? "(".concat(words.join(" "), ")") : words.join(" "); }, /** * Print number in format defined by engine * @param {string} engine Engine to use * @param {string} ref Branch ref (e.g. 1) * @return {string} Formatted number */ printNumber: function printNumber(engine, ref) { // Get line number format for engine var number = ref; switch (engine) { case 'PubMed full': case 'PubMed abbreviation': case 'Cochrane Library': case 'Embase (Elsevier)': case 'Web of Science': case 'WoS Advanced': case 'Scopus (basic search)': case 'Scopus (advanced search)': //HTA case 'International HTA Database': number = "#" + ref; break; case 'Ovid MEDLINE': case 'PsycInfo (Ovid)': case 'ProQuest Health and Medical': number = ref; break; case 'CINAHL (Ebsco)': case 'Business Source Ultimate': case 'PsycInfo (Ebsco)': case 'SPORTDiscus': number = "S" + ref; break; default: } return number; }, /** * Determine if a phrase needs to be enclosed within speachmarks and return the result * @param {Object} branch Phrase branch to examine * @param {string} engine Optional engine ID to examine for other enclose methods * @param {boolean} highlighting Optional bool to determine if html color styling is added * @return {string} The phrase enclosed as needed */ quotePhrase: function quotePhrase(branch, engine, settings) { var text = _lodash["default"].trimEnd(branch.content); var space = /\s/.test(text); // Apply wildcard replacements if (settings.replaceWildcards) { var replaceObj = {}; switch (engine) { case 'PubMed full': case 'PubMed abbreviation': replaceObj = { '$': settings.highlighting ? tools.createTooltip("*", "As PubMed does not support single character truncation a wildcard is used here", "highlight") : '*', '?': settings.highlighting ? tools.createTooltip("*", "As PubMed does not 0 or 1 character truncation a wildcard is used here", "highlight") : '*', '#': settings.highlighting ? tools.createTooltip("*", "As PubMed does not single character wildcards a wildcard is used here", "highlight") : '*' }; break; case 'Ovid MEDLINE': break; // Nothing needed case 'Cochrane Library': if (space) { text = tools.wildCardCochrane(text, settings.highlighting); } replaceObj = { '$': settings.highlighting ? tools.createTooltip("?", "As Cochrane does not support single character truncation, the 0 or 1 character truncation is used here.", "highlight") : '?', '#': '?' }; return tools.multiReplace(text, replaceObj); // Return here to prevent duplicate quotes case 'Embase (Elsevier)': case 'Web of Science': case 'WoS Advanced': replaceObj = { '$': '?', '?': '$', '#': '?' }; break; case 'CINAHL (Ebsco)': replaceObj = { '$': '?', '?': '#', '#': '?' }; break; case 'Business Source Ultimate': replaceObj = { '$': '?', '?': '#', '#': '?' }; break; case 'PsycInfo (Ebsco)': replaceObj = { '$': '?', '?': '#', '#': '?' }; break; case 'Scopus (basic search)': case 'Scopus (advanced search)': // space = true; //Always include quotes with scopus to make phrase a "loose phrase" replaceObj = { '$': '?', '?': settings.highlighting ? tools.createTooltip("*", "0 or 1 character truncation is not available. The multiple character wildcard symbol has been substituted.", "highlight") : '*', '#': '?' }; break; case 'PsycInfo (Ovid)': replaceObj = { '$': '#' }; break; case 'ProQuest Health and Medical': replaceObj = { '$': '?', '#': '?' }; break; case 'SPORTDiscus': if (!settings.testing) { space = true; //Always include quotes with SPORTDiscus } replaceObj = { '$': '#?', '?': '#' }; break; case 'Informit Health Collection': replaceObj = { '$': '?', '?': '*1' }; break; //HTA case 'International HTA Database': replaceObj = { '$': settings.highlighting ? tools.createTooltip("*", "As INAHTA does not support single character truncation a wildcard is used here", "highlight") : '*', '?': settings.highlighting ? tools.createTooltip("*", "As INAHTA does not support single character truncation a wildcard is used here", "highlight") : '*', '#': settings.highlighting ? tools.createTooltip("*", "As INAHTA does not support single character truncation a wildcard is used here", "highlight") : '*' }; break; } text = tools.multiReplace(text, replaceObj); } return engine == 'Embase (Elsevier)' ? space ? settings.highlighting ? "<font color='DarkBlue'>'" + text + "'</font>" : "'" + text + "'" : text : space ? settings.highlighting ? '<font color="DarkBlue">"' + text + '"</font>' : '"' + text + '"' : text; }, /** * Convert the '$or' / '$and' nodes within a tree into a nested structure * This function will also flatten identical branches (i.e. run-on multiple $and / $or into one array) * @param {Object} tree The object tree to recombine * @returns {Object} The recombined tree */ renestConditions: function renestConditions(tree) { if (!_lodash["default"].isArray(tree)) return tree; // Not an array - skip // Transform arrays of the form: [X1, $or/$and, X2] => {$or/$and: [X1, X2]} return tree.reduce(function (res, branch, index, arr) { var firstKey = (0, _lodash["default"])(branch).keys().first(); if (firstKey == '$or' || firstKey == '$and') { // Is a combinator var expression = {}; expression[firstKey] = [res.pop(), // Right side is the last thing we added to the buffer arr.splice(index + 1, 1)[0] // Left side is the next thing we're going to look at in the array ]; res.push(expression); } else { // Unknown - just push to array and carry on processing res.push(branch); } return res; }, []); }, /** * Combine multiple run-on $and / $or conditional branches into one branch * This function is a companion function to renestConditions and should be called directly afterwards if needed * @param {Object} tree The tree to traverse * @param {Object} [options] Additional options to accept * @param {number} [options.depth=10] The maximum depth to traverse before giving up, set to 0 to infinitely recurse * @return {Object} The collapsed tree * @example * {left, joinAnd, right} => {joinAnd: [left, right]} * @example * {foo, joinOr, bar, joinOr, baz} => {joinOr: [foo, bar, baz]} */ combineConditions: function combineConditions(tree, options) { var settings = _lodash["default"].defaults(options, { depth: 10 }); var collapses = []; var traverseTree = function traverseTree(branch) { var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; // Recurse into each tree node and make a bottom-up list of nodes we need to collapse _lodash["default"].forEach(branch, function (v, k) { // Use _.map if its an array and _.mapValues if we're examining an object if (_lodash["default"].isObject(v)) { var firstKey = (0, _lodash["default"])(branch).keys().first(); if (path.length > 1 && (firstKey == '$or' || firstKey == '$and')) { // Mark for cleanup later (when we can do a bottom-up traversal) var lastKey = _lodash["default"].findLast(collapses, function (i) { return i.key == '$and' || i.key == '$or'; }); // Collapse only identical keys if (!lastKey || lastKey.key == firstKey) { collapses.push({ key: firstKey, path: path }); } } if (settings.depth && path.length > settings.depth) return; // Stop recursing after depth has been reached traverseTree(v, path.concat([k])); } }); }; traverseTree(tree); collapses.forEach(function (collapse) { var parent = _lodash["default"].get(tree, collapse.path.slice(0, -1)); var child = _lodash["default"].get(tree, collapse.path.concat([collapse.key])); if (!child || !parent || !parent.length) return; var child2 = parent[1]; if (child2) child.push(child2); // Wrap $or conditions (that have an '$and' parent) in an object {{{ var lastParent = (0, _lodash["default"])(collapse.path).slice(0, -1).findLast(_lodash["default"].isString); if (lastParent && lastParent == '$and' && collapse.key == '$or') child = { $or: child }; // }}} _lodash["default"].set(tree, collapse.path.slice(0, -1), child); }); return tree; }, /** * Create a tooltip with a specified message * @param {string} content Content to append tooltip to * @param {string} message Message to contain inside tooltip * @param {string} css CSS class to use */ createTooltip: function createTooltip(content, message, css) { css = typeof css !== 'undefined' ? css : "black-underline"; return "<span class=\"" + css + '" v-tooltip="`' + message + '`">' + content + '</span>'; }, /** * Create a popover with options to replace empty field tags with specified field tag * @param {string} content Content to append popover to */ createPopover: function createPopover(content, offset) { return '<v-popover offset="8" placement="right">' + '<span class="blue-underline">' + content + '</span>' + '<template slot="popover">' + '<h3 class="popover-header">Add Field Tag</h3>' + '<input class="tooltip-content" v-model="customField" placeholder="Field tag" />' + '<div class="replace-all">' + '<input type="checkbox" id="checkbox" v-model="replaceAll">' + '<label for="checkbox">Replace All</label>' + '</div>' + '<div class="replace-buttons">' + '<button v-on:click="replaceFields(customField, replaceAll, ' + offset + ')" type="button" class="btn btn-primary">Replace</button>' + '<button v-close-popover type="button" class="btn btn-dark">Close</button>' + '</div>' + '</template>' + '</v-popover>'; } }; var _default = tools; exports["default"] = _default;