UNPKG

bluebutton

Version:

BlueButton.js helps developers navigate complex health data with ease.

1,701 lines (1,515 loc) 116 kB
(function(root, factory) { if(typeof exports === 'object') { module.exports = factory(); } else if(typeof define === 'function' && define.amd) { define([], factory); } else { root['BlueButton'] = factory(); } }(this, function() { /* BlueButton.js -- 0.4.2 */ /* * ... */ /* exported Core */ var Core = (function () { /* * ... */ var parseData = function (source) { source = stripWhitespace(source); if (source.charAt(0) === '<') { try { return Core.XML.parse(source); } catch (e) { if (console.log) { console.log("File looked like it might be XML but couldn't be parsed."); } } } try { return JSON.parse(source); } catch (e) { if (console.error) { console.error("Error: Cannot parse this file. BB.js only accepts valid XML " + "(for parsing) or JSON (for generation). If you are attempting to provide " + "XML or JSON, please run your data through a validator to see if it is malformed.\n"); } throw e; } }; /* * Removes leading and trailing whitespace from a string */ var stripWhitespace = function (str) { if (!str) { return str; } return str.replace(/^\s+|\s+$/g,''); }; /* * A wrapper around JSON.stringify which allows us to produce customized JSON. * * See https://developer.mozilla.org/en-US/docs/Web/ * JavaScript/Guide/Using_native_JSON#The_replacer_parameter * for documentation on the replacerFn. */ var json = function () { var datePad = function(number) { if (number < 10) { return '0' + number; } return number; }; var replacerFn = function(key, value) { /* By default, Dates are output as ISO Strings like "2014-01-03T08:00:00.000Z." This is * tricky when all we have is a date (not a datetime); JS sadly ignores that distinction. * * To paper over this JS wart, we use two different JSON formats for dates and datetimes. * This is a little ugly but makes sure that the dates/datetimes mostly just parse * correclty for clients: * * 1. Datetimes are rendered as standard ISO strings, without the misleading millisecond * precision (misleading because we don't have it): YYYY-MM-DDTHH:mm:ssZ * 2. Dates are rendered as MM/DD/YYYY. This ensures they are parsed as midnight local-time, * no matter what local time is, and therefore ensures the date is always correct. * Outputting "YYYY-MM-DD" would lead most browsers/node to assume midnight UTC, which * means "2014-04-27" suddenly turns into "04/26/2014 at 5PM" or just "04/26/2014" * if you format it as a date... * * See http://stackoverflow.com/questions/2587345/javascript-date-parse and * http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html * for more on this issue. */ var originalValue = this[key]; // a Date if ( value && (originalValue instanceof Date) && !isNaN(originalValue.getTime()) ) { // If while parsing we indicated that there was time-data specified, or if we see // non-zero values in the hour/minutes/seconds/millis fields, output a datetime. if (originalValue._parsedWithTimeData || originalValue.getHours() || originalValue.getMinutes() || originalValue.getSeconds() || originalValue.getMilliseconds()) { // Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/ // Reference/Global_Objects/Date/toISOString return originalValue.getUTCFullYear() + '-' + datePad( originalValue.getUTCMonth() + 1 ) + '-' + datePad( originalValue.getUTCDate() ) + 'T' + datePad( originalValue.getUTCHours() ) + ':' + datePad( originalValue.getUTCMinutes() ) + ':' + datePad( originalValue.getUTCSeconds() ) + 'Z'; } // We just have a pure date return datePad( originalValue.getMonth() + 1 ) + '/' + datePad( originalValue.getDate() ) + '/' + originalValue.getFullYear(); } return value; }; return JSON.stringify(this, replacerFn, 2); }; /* * Removes all `null` properties from an object. */ var trim = function (o) { var y; for (var x in o) { if (o.hasOwnProperty(x)) { y = o[x]; // if (y === null || (y instanceof Object && Object.keys(y).length == 0)) { if (y === null) { delete o[x]; } if (y instanceof Object) y = trim(y); } } return o; }; return { parseData: parseData, stripWhitespace: stripWhitespace, json: json, trim: trim }; })(); ; /* * ... */ Core.Codes = (function () { /* * Administrative Gender (HL7 V3) * http://phinvads.cdc.gov/vads/ViewValueSet.action?id=8DE75E17-176B-DE11-9B52-0015173D1785 * OID: 2.16.840.1.113883.1.11.1 */ var GENDER_MAP = { 'F': 'female', 'M': 'male', 'UN': 'undifferentiated' }; /* * Marital Status (HL7) * http://phinvads.cdc.gov/vads/ViewValueSet.action?id=46D34BBC-617F-DD11-B38D-00188B398520 * OID: 2.16.840.1.114222.4.11.809 */ var MARITAL_STATUS_MAP = { 'N': 'annulled', 'C': 'common law', 'D': 'divorced', 'P': 'domestic partner', 'I': 'interlocutory', 'E': 'legally separated', 'G': 'living together', 'M': 'married', 'O': 'other', 'R': 'registered domestic partner', 'A': 'separated', 'S': 'single', 'U': 'unknown', 'B': 'unmarried', 'T': 'unreported', 'W': 'widowed' }; /* * Religious Affiliation (HL7 V3) * https://phinvads.cdc.gov/vads/ViewValueSet.action?id=6BFDBFB5-A277-DE11-9B52-0015173D1785 * OID: 2.16.840.1.113883.5.1076 */ var RELIGION_MAP = { "1001": "adventist", "1002": "african religions", "1003": "afro-caribbean religions", "1004": "agnosticism", "1005": "anglican", "1006": "animism", "1061": "assembly of god", "1007": "atheism", "1008": "babi & baha'i faiths", "1009": "baptist", "1010": "bon", "1062": "brethren", "1011": "cao dai", "1012": "celticism", "1013": "christian (non-catholic, non-specific)", "1063": "christian scientist", "1064": "church of christ", "1065": "church of god", "1014": "confucianism", "1066": "congregational", "1015": "cyberculture religions", "1067": "disciples of christ", "1016": "divination", "1068": "eastern orthodox", "1069": "episcopalian", "1070": "evangelical covenant", "1017": "fourth way", "1018": "free daism", "1071": "friends", "1072": "full gospel", "1019": "gnosis", "1020": "hinduism", "1021": "humanism", "1022": "independent", "1023": "islam", "1024": "jainism", "1025": "jehovah's witnesses", "1026": "judaism", "1027": "latter day saints", "1028": "lutheran", "1029": "mahayana", "1030": "meditation", "1031": "messianic judaism", "1073": "methodist", "1032": "mitraism", "1074": "native american", "1075": "nazarene", "1033": "new age", "1034": "non-roman catholic", "1035": "occult", "1036": "orthodox", "1037": "paganism", "1038": "pentecostal", "1076": "presbyterian", "1039": "process, the", "1077": "protestant", "1078": "protestant, no denomination", "1079": "reformed", "1040": "reformed/presbyterian", "1041": "roman catholic church", "1080": "salvation army", "1042": "satanism", "1043": "scientology", "1044": "shamanism", "1045": "shiite (islam)", "1046": "shinto", "1047": "sikism", "1048": "spiritualism", "1049": "sunni (islam)", "1050": "taoism", "1051": "theravada", "1081": "unitarian universalist", "1052": "unitarian-universalism", "1082": "united church of christ", "1053": "universal life church", "1054": "vajrayana (tibetan)", "1055": "veda", "1056": "voodoo", "1057": "wicca", "1058": "yaohushua", "1059": "zen buddhism", "1060": "zoroastrianism" }; /* * Race & Ethnicity (HL7 V3) * Full list at http://phinvads.cdc.gov/vads/ViewCodeSystem.action?id=2.16.840.1.113883.6.238 * OID: 2.16.840.1.113883.6.238 * * Abbreviated list closer to real usage at: (Race / Ethnicity) * https://phinvads.cdc.gov/vads/ViewValueSet.action?id=67D34BBC-617F-DD11-B38D-00188B398520 * https://phinvads.cdc.gov/vads/ViewValueSet.action?id=35D34BBC-617F-DD11-B38D-00188B398520 */ var RACE_ETHNICITY_MAP = { '2028-9': 'asian', '2054-5': 'black or african american', '2135-2': 'hispanic or latino', '2076-8': 'native', '2186-5': 'not hispanic or latino', '2131-1': 'other', '2106-3': 'white' }; /* * Role (HL7 V3) * https://phinvads.cdc.gov/vads/ViewCodeSystem.action?id=2.16.840.1.113883.5.111 * OID: 2.16.840.1.113883.5.111 */ var ROLE_MAP = { "ACC": "accident site", "ACHFID": "accreditation location identifier", "ACTMIL": "active duty military", "ALL": "allergy clinic", "AMB": "ambulance", "AMPUT": "amputee clinic", "ANTIBIOT": "antibiotic", "ASSIST": "assistive non-person living subject", "AUNT": "aunt", "B": "blind", "BF": "beef", "BILL": "billing contact", "BIOTH": "biotherapeutic non-person living subject", "BL": "broiler", "BMTC": "bone marrow transplant clinic", "BMTU": "bone marrow transplant unit", "BR": "breeder", "BREAST": "breast clinic", "BRO": "brother", "BROINLAW": "brother-in-law", "C": "calibrator", "CANC": "child and adolescent neurology clinic", "CAPC": "child and adolescent psychiatry clinic", "CARD": "ambulatory health care facilities; clinic/center; rehabilitation: cardiac facilities", "CAS": "asylum seeker", "CASM": "single minor asylum seeker", "CATH": "cardiac catheterization lab", "CCO": "clinical companion", "CCU": "coronary care unit", "CHEST": "chest unit", "CHILD": "child", "CHLDADOPT": "adopted child", "CHLDFOST": "foster child", "CHLDINLAW": "child in-law", "CHR": "chronic care facility", "CLAIM": "claimant", "CN": "national", "CNRP": "non-country member without residence permit", "CNRPM": "non-country member minor without residence permit", "CO": "companion", "COAG": "coagulation clinic", "COCBEN": "continuity of coverage beneficiary", "COMM": "community location", "COMMUNITYLABORATORY": "community laboratory", "COUSN": "cousin", "CPCA": "permit card applicant", "CRIMEVIC": "crime victim", "CRP": "non-country member with residence permit", "CRPM": "non-country member minor with residence permit", "CRS": "colon and rectal surgery clinic", "CSC": "community service center", "CVDX": "cardiovascular diagnostics or therapeutics unit", "DA": "dairy", "DADDR": "delivery address", "DAU": "natural daughter", "DAUADOPT": "adopted daughter", "DAUC": "daughter", "DAUFOST": "foster daughter", "DAUINLAW": "daughter in-law", "DC": "therapeutic class", "DEBR": "debridement", "DERM": "dermatology clinic", "DIFFABL": "differently abled", "DOMPART": "domestic partner", "DPOWATT": "durable power of attorney", "DR": "draft", "DU": "dual", "DX": "diagnostics or therapeutics unit", "E": "electronic qc", "ECHO": "echocardiography lab", "ECON": "emergency contact", "ENDO": "endocrinology clinic", "ENDOS": "endoscopy lab", "ENROLBKR": "enrollment broker", "ENT": "otorhinolaryngology clinic", "EPIL": "epilepsy unit", "ER": "emergency room", "ERL": "enrollment", "ETU": "emergency trauma unit", "EXCEST": "executor of estate", "EXT": "extended family member", "F": "filler proficiency", "FAMDEP": "family dependent", "FAMMEMB": "family member", "FI": "fiber", "FMC": "family medicine clinic", "FRND": "unrelated friend", "FSTUD": "full-time student", "FTH": "father", "FTHINLAW": "father-in-law", "FULLINS": "fully insured coverage sponsor", "G": "group", "GACH": "hospitals; general acute care hospital", "GD": "generic drug", "GDF": "generic drug form", "GDS": "generic drug strength", "GDSF": "generic drug strength form", "GGRFTH": "great grandfather", "GGRMTH": "great grandmother", "GGRPRN": "great grandparent", "GI": "gastroenterology clinic", "GIDX": "gastroenterology diagnostics or therapeutics lab", "GIM": "general internal medicine clinic", "GRFTH": "grandfather", "GRMTH": "grandmother", "GRNDCHILD": "grandchild", "GRNDDAU": "granddaughter", "GRNDSON": "grandson", "GRPRN": "grandparent", "GT": "guarantor", "GUADLTM": "guardian ad lidem", "GUARD": "guardian", "GYN": "gynecology clinic", "HAND": "hand clinic", "HANDIC": "handicapped dependent", "HBRO": "half-brother", "HD": "hemodialysis unit", "HEM": "hematology clinic", "HLAB": "hospital laboratory", "HOMEHEALTH": "home health", "HOSP": "hospital", "HPOWATT": "healthcare power of attorney", "HRAD": "radiology unit", "HSIB": "half-sibling", "HSIS": "half-sister", "HTN": "hypertension clinic", "HU": "hospital unit", "HUSB": "husband", "HUSCS": "specimen collection site", "ICU": "intensive care unit", "IEC": "impairment evaluation center", "INDIG": "member of an indigenous people", "INFD": "infectious disease clinic", "INJ": "injured plaintiff", "INJWKR": "injured worker", "INLAB": "inpatient laboratory", "INPHARM": "inpatient pharmacy", "INV": "infertility clinic", "JURID": "jurisdiction location identifier", "L": "pool", "LABORATORY": "laboratory", "LOCHFID": "local location identifier", "LY": "layer", "LYMPH": "lympedema clinic", "MAUNT": "maternalaunt", "MBL": "medical laboratory", "MCOUSN": "maternalcousin", "MGDSF": "manufactured drug strength form", "MGEN": "medical genetics clinic", "MGGRFTH": "maternalgreatgrandfather", "MGGRMTH": "maternalgreatgrandmother", "MGGRPRN": "maternalgreatgrandparent", "MGRFTH": "maternalgrandfather", "MGRMTH": "maternalgrandmother", "MGRPRN": "maternalgrandparent", "MHSP": "military hospital", "MIL": "military", "MOBL": "mobile unit", "MT": "meat", "MTH": "mother", "MTHINLAW": "mother-in-law", "MU": "multiplier", "MUNCLE": "maternaluncle", "NBOR": "neighbor", "NBRO": "natural brother", "NCCF": "nursing or custodial care facility", "NCCS": "neurology critical care and stroke unit", "NCHILD": "natural child", "NEPH": "nephrology clinic", "NEPHEW": "nephew", "NEUR": "neurology clinic", "NFTH": "natural father", "NFTHF": "natural father of fetus", "NIECE": "niece", "NIENEPH": "niece/nephew", "NMTH": "natural mother", "NOK": "next of kin", "NPRN": "natural parent", "NS": "neurosurgery unit", "NSIB": "natural sibling", "NSIS": "natural sister", "O": "operator proficiency", "OB": "obstetrics clinic", "OF": "outpatient facility", "OMS": "oral and maxillofacial surgery clinic", "ONCL": "medical oncology clinic", "OPH": "opthalmology clinic", "OPTC": "optometry clinic", "ORG": "organizational contact", "ORTHO": "orthopedics clinic", "OUTLAB": "outpatient laboratory", "OUTPHARM": "outpatient pharmacy", "P": "patient", "PAINCL": "pain clinic", "PATHOLOGIST": "pathologist", "PAUNT": "paternalaunt", "PAYOR": "payor contact", "PC": "primary care clinic", "PCOUSN": "paternalcousin", "PEDC": "pediatrics clinic", "PEDCARD": "pediatric cardiology clinic", "PEDE": "pediatric endocrinology clinic", "PEDGI": "pediatric gastroenterology clinic", "PEDHEM": "pediatric hematology clinic", "PEDHO": "pediatric oncology clinic", "PEDICU": "pediatric intensive care unit", "PEDID": "pediatric infectious disease clinic", "PEDNEPH": "pediatric nephrology clinic", "PEDNICU": "pediatric neonatal intensive care unit", "PEDRHEUM": "pediatric rheumatology clinic", "PEDU": "pediatric unit", "PGGRFTH": "paternalgreatgrandfather", "PGGRMTH": "paternalgreatgrandmother", "PGGRPRN": "paternalgreatgrandparent", "PGRFTH": "paternalgrandfather", "PGRMTH": "paternalgrandmother", "PGRPRN": "paternalgrandparent", "PH": "policy holder", "PHARM": "pharmacy", "PHLEBOTOMIST": "phlebotomist", "PHU": "psychiatric hospital unit", "PL": "pleasure", "PLS": "plastic surgery clinic", "POD": "podiatry clinic", "POWATT": "power of attorney", "PRC": "pain rehabilitation center", "PREV": "preventive medicine clinic", "PRN": "parent", "PRNINLAW": "parent in-law", "PROCTO": "proctology clinic", "PROFF": "provider's office", "PROG": "program eligible", "PROS": "prosthodontics clinic", "PSI": "psychology clinic", "PSTUD": "part-time student", "PSY": "psychiatry clinic", "PSYCHF": "psychiatric care facility", "PT": "patient", "PTRES": "patient's residence", "PUNCLE": "paternaluncle", "Q": "quality control", "R": "replicate", "RADDX": "radiology diagnostics or therapeutics unit", "RADO": "radiation oncology unit", "RC": "racing", "RESPRSN": "responsible party", "RETIREE": "retiree", "RETMIL": "retired military", "RH": "rehabilitation hospital", "RHAT": "addiction treatment center", "RHEUM": "rheumatology clinic", "RHII": "intellectual impairment center", "RHMAD": "parents with adjustment difficulties center", "RHPI": "physical impairment center", "RHPIH": "physical impairment - hearing center", "RHPIMS": "physical impairment - motor skills center", "RHPIVS": "physical impairment - visual skills center", "RHU": "rehabilitation hospital unit", "RHYAD": "youths with adjustment difficulties center", "RNEU": "neuroradiology unit", "ROOM": "roommate", "RTF": "residential treatment facility", "SCHOOL": "school", "SCN": "screening", "SEE": "seeing", "SELF": "self", "SELFINS": "self insured coverage sponsor", "SH": "show", "SIB": "sibling", "SIBINLAW": "sibling in-law", "SIGOTHR": "significant other", "SIS": "sister", "SISINLAW": "sister-in-law", "SLEEP": "sleep disorders unit", "SNF": "skilled nursing facility", "SNIFF": "sniffing", "SON": "natural son", "SONADOPT": "adopted son", "SONC": "son", "SONFOST": "foster son", "SONINLAW": "son in-law", "SPMED": "sports medicine clinic", "SPON": "sponsored dependent", "SPOWATT": "special power of attorney", "SPS": "spouse", "STPBRO": "stepbrother", "STPCHLD": "step child", "STPDAU": "stepdaughter", "STPFTH": "stepfather", "STPMTH": "stepmother", "STPPRN": "step parent", "STPSIB": "step sibling", "STPSIS": "stepsister", "STPSON": "stepson", "STUD": "student", "SU": "surgery clinic", "SUBJECT": "self", "SURF": "substance use rehabilitation facility", "THIRDPARTY": "third party", "TPA": "third party administrator", "TR": "transplant clinic", "TRAVEL": "travel and geographic medicine clinic", "TRB": "tribal member", "UMO": "utilization management organization", "UNCLE": "uncle", "UPC": "underage protection center", "URO": "urology clinic", "V": "verifying", "VET": "veteran", "VL": "veal", "WARD": "ward", "WIFE": "wife", "WL": "wool", "WND": "wound clinic", "WO": "working", "WORK": "work site", }; var PROBLEM_STATUS_MAP = { "55561003": "active", "73425007": "inactive", "413322009": "resolved" }; // copied from _.invert to avoid making browser users include all of underscore var invertKeys = function(obj) { var result = {}; var keys = Object.keys(obj); for (var i = 0, length = keys.length; i < length; i++) { result[obj[keys[i]]] = keys[i]; } return result; }; var lookupFnGenerator = function(map) { return function(key) { return map[key] || null; }; }; var reverseLookupFnGenerator = function(map) { return function(key) { if (!key) { return null; } var invertedMap = invertKeys(map); key = key.toLowerCase(); return invertedMap[key] || null; }; }; return { gender: lookupFnGenerator(GENDER_MAP), reverseGender: reverseLookupFnGenerator(GENDER_MAP), maritalStatus: lookupFnGenerator(MARITAL_STATUS_MAP), reverseMaritalStatus: reverseLookupFnGenerator(MARITAL_STATUS_MAP), religion: lookupFnGenerator(RELIGION_MAP), reverseReligion: reverseLookupFnGenerator(RELIGION_MAP), raceEthnicity: lookupFnGenerator(RACE_ETHNICITY_MAP), reverseRaceEthnicity: reverseLookupFnGenerator(RACE_ETHNICITY_MAP), role: lookupFnGenerator(ROLE_MAP), reverseRole: reverseLookupFnGenerator(ROLE_MAP), problemStatus: lookupFnGenerator(PROBLEM_STATUS_MAP), reverseProblemStatus: reverseLookupFnGenerator(PROBLEM_STATUS_MAP) }; })(); ; /* * ... */ Core.XML = (function () { /* * A function used to wrap DOM elements in an object so methods can be added * to the element object. IE8 does not allow methods to be added directly to * DOM objects. */ var wrapElement = function (el) { function wrapElementHelper(currentEl) { return { el: currentEl, template: template, content: content, tag: tag, immediateChildTag: immediateChildTag, elsByTag: elsByTag, attr: attr, boolAttr: boolAttr, val: val, isEmpty: isEmpty }; } // el is an array of elements if (el.length) { var els = []; for (var i = 0; i < el.length; i++) { els.push(wrapElementHelper(el[i])); } return els; // el is a single element } else { return wrapElementHelper(el); } }; /* * Find element by tag name, then attribute value. */ var tagAttrVal = function (el, tag, attr, value) { el = el.getElementsByTagName(tag); for (var i = 0; i < el.length; i++) { if (el[i].getAttribute(attr) === value) { return el[i]; } } }; /* * Search for a template ID, and return its parent element. * Example: * <templateId root="2.16.840.1.113883.10.20.22.2.17"/> * Can be found using: * el = dom.template('2.16.840.1.113883.10.20.22.2.17'); */ var template = function (templateId) { var el = tagAttrVal(this.el, 'templateId', 'root', templateId); if (!el) { return emptyEl(); } else { return wrapElement(el.parentNode); } }; /* * Search for a content tag by "ID", and return it as an element. * These are used in the unstructured versions of each section but * referenced from the structured version sometimes. * Example: * <content ID="UniqueNameReferencedElsewhere"/> * Can be found using: * el = dom.content('UniqueNameReferencedElsewhere'); * * We can't use `getElementById` because `ID` (the standard attribute name * in this context) is not the same attribute as `id` in XML, so there are no matches */ var content = function (contentId) { var el = tagAttrVal(this.el, 'content', 'ID', contentId); if (!el) { // check the <td> tag too, which isn't really correct but // will inevitably be used sometimes because it looks like very // normal HTML to put the data directly in a <td> el = tagAttrVal(this.el, 'td', 'ID', contentId); } if (!el) { // Ugh, Epic uses really non-standard locations. el = tagAttrVal(this.el, 'caption', 'ID', contentId) || tagAttrVal(this.el, 'paragraph', 'ID', contentId) || tagAttrVal(this.el, 'tr', 'ID', contentId) || tagAttrVal(this.el, 'item', 'ID', contentId); } if (!el) { return emptyEl(); } else { return wrapElement(el); } }; /* * Search for the first occurrence of an element by tag name. */ var tag = function (tag) { var el = this.el.getElementsByTagName(tag)[0]; if (!el) { return emptyEl(); } else { return wrapElement(el); } }; /* * Like `tag`, except it will only count a tag that is an immediate child of `this`. * This is useful for tags like "text" which A. may not be present for a given location * in every document and B. have a very different meaning depending on their positioning * * <parent> * <target></target> * </parent> * vs. * <parent> * <intermediate> * <target></target> * </intermediate> * </parent> * parent.immediateChildTag('target') will have a result in the first case but not in the second. */ var immediateChildTag = function (tag) { var els = this.el.getElementsByTagName(tag); if (!els) { return null; } for (var i = 0; i < els.length; i++) { if (els[i].parentNode === this.el) { return wrapElement(els[i]); } } return emptyEl(); }; /* * Search for all elements by tag name. */ var elsByTag = function (tag) { return wrapElement(this.el.getElementsByTagName(tag)); }; var unescapeSpecialChars = function(s) { if (!s) { return s; } return s.replace(/&lt;/g, '<') .replace(/&gt;/g, '>') .replace(/&amp;/g, '&') .replace(/&quot;/g, '"') .replace(/&apos;/g, "'"); }; /* * Retrieve the element's attribute value. Example: * value = el.attr('displayName'); * * The browser and jsdom return "null" for empty attributes; * xmldom (which we now use because it's faster / can be explicitly * told to parse malformed XML as XML anyways), return the empty * string instead, so we fix that here. */ var attr = function (attrName) { if (!this.el) { return null; } var attrVal = this.el.getAttribute(attrName); if (attrVal) { return unescapeSpecialChars(attrVal); } return null; }; /* * Wrapper for attr() for retrieving boolean attributes; * a raw call attr() will return Strings, which can be unexpected, * since the string 'false' will by truthy */ var boolAttr = function (attrName) { var rawAttr = this.attr(attrName); if (rawAttr === 'true' || rawAttr === '1') { return true; } return false; }; /* * Retrieve the element's value. For example, if the element is: * <city>Madison</city> * Use: * value = el.tag('city').val(); * * This function also knows how to retrieve the value of <reference> tags, * which can store their content in a <content> tag in a totally different * part of the document. */ var val = function () { if (!this.el) { return null; } if (!this.el.childNodes || !this.el.childNodes.length) { return null; } var textContent = this.el.textContent; // if there's no text value here and the only thing inside is a // <reference> tag, see if there's a linked <content> tag we can // get something out of if (!Core.stripWhitespace(textContent)) { var contentId; // "no text value" might mean there's just a reference tag if (this.el.childNodes.length === 1 && this.el.childNodes[0].tagName === 'reference') { contentId = this.el.childNodes[0].getAttribute('value'); // or maybe a newlines on top/above the reference tag } else if (this.el.childNodes.length === 3 && this.el.childNodes[1].tagName === 'reference') { contentId = this.el.childNodes[1].getAttribute('value'); } else { return unescapeSpecialChars(textContent); } if (contentId && contentId[0] === '#') { contentId = contentId.slice(1); // get rid of the '#' var docRoot = wrapElement(this.el.ownerDocument); var contentTag = docRoot.content(contentId); return contentTag.val(); } } return unescapeSpecialChars(textContent); }; /* * Creates and returns an empty DOM element with tag name "empty": * <empty></empty> */ var emptyEl = function () { var el = doc.createElement('empty'); return wrapElement(el); }; /* * Determines if the element is empty, i.e.: * <empty></empty> * This element is created by function `emptyEL`. */ var isEmpty = function () { if (this.el.tagName.toLowerCase() === 'empty') { return true; } else { return false; } }; /* * Cross-browser XML parsing supporting IE8+ and Node.js. */ var parse = function (data) { // XML data must be a string if (!data || typeof data !== "string") { console.log("BB Error: XML data is not a string"); return null; } var xml, parser; // Node if (isNode) { parser = new (xmldom.DOMParser)(); xml = parser.parseFromString(data, "text/xml"); // Browser } else { // Standard parser if (window.DOMParser) { parser = new DOMParser(); xml = parser.parseFromString(data, "text/xml"); // IE } else { try { xml = new ActiveXObject("Microsoft.XMLDOM"); xml.async = "false"; xml.loadXML(data); } catch (e) { console.log("BB ActiveX Exception: Could not parse XML"); } } } if (!xml || !xml.documentElement || xml.getElementsByTagName("parsererror").length) { console.log("BB Error: Could not parse XML"); return null; } return wrapElement(xml); }; // Establish the root object, `window` in the browser, or `global` in Node. var root = this, xmldom, isNode = false, doc = root.document; // Will be `undefined` if we're in Node // Check if we're in Node. If so, pull in `xmldom` so we can simulate the DOM. if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { isNode = true; xmldom = require("xmldom"); doc = new xmldom.DOMImplementation().createDocument(); } } return { parse: parse }; })(); ; /* * ... */ /* exported Documents */ var Documents = (function () { /* * ... */ var detect = function (data) { if (!data.template) { return 'json'; } if (!data.template('2.16.840.1.113883.3.88.11.32.1').isEmpty()) { return 'c32'; } else if(!data.template('2.16.840.1.113883.10.20.22.1.1').isEmpty()) { return 'ccda'; } }; /* * Get entries within an element (with tag name 'entry'), adds an `each` function */ var entries = function () { var each = function (callback) { for (var i = 0; i < this.length; i++) { callback(this[i]); } }; var els = this.elsByTag('entry'); els.each = each; return els; }; /* * Parses an HL7 date in String form and creates a new Date object. * * TODO: CCDA dates can be in form: * <effectiveTime value="20130703094812"/> * ...or: * <effectiveTime> * <low value="19630617120000"/> * <high value="20110207100000"/> * </effectiveTime> * For the latter, parseDate will not be given type `String` * and will return `null`. */ var parseDate = function (str) { if (!str || typeof str !== 'string') { return null; } // Note: months start at 0 (so January is month 0) // e.g., value="1999" translates to Jan 1, 1999 if (str.length === 4) { return new Date(str, 0, 1); } var year = str.substr(0, 4); // subtract 1 from the month since they're zero-indexed var month = parseInt(str.substr(4, 2), 10) - 1; // days are not zero-indexed. If we end up with the day 0 or '', // that will be equivalent to the last day of the previous month var day = str.substr(6, 2) || 1; // check for time info (the presence of at least hours and mins after the date) if (str.length >= 12) { var hour = str.substr(8, 2); var min = str.substr(10, 2); var secs = str.substr(12, 2); // check for timezone info (the presence of chars after the seconds place) if (str.length > 14) { // _utcOffsetFromString will return 0 if there's no utc offset found. var utcOffset = _utcOffsetFromString(str.substr(14)); // We subtract that offset from the local time to get back to UTC // (e.g., if we're -480 mins behind UTC, we add 480 mins to get back to UTC) min = _toInt(min) - utcOffset; } var date = new Date(Date.UTC(year, month, day, hour, min, secs)); // This flag lets us output datetime-precision in our JSON even if the time happens // to translate to midnight local time. If we clone the date object, it is not // guaranteed to survive. date._parsedWithTimeData = true; return date; } return new Date(year, month, day); }; // These regexes and the two functions below are copied from moment.js // http://momentjs.com/ // https://github.com/moment/moment/blob/develop/LICENSE var parseTimezoneChunker = /([\+\-]|\d\d)/gi; var parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z function _utcOffsetFromString(string) { string = string || ''; var possibleTzMatches = (string.match(parseTokenTimezone) || []), tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [], parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0], minutes = +(parts[1] * 60) + _toInt(parts[2]); return parts[0] === '+' ? minutes : -minutes; } function _toInt(argumentForCoercion) { var coercedNumber = +argumentForCoercion, value = 0; if (coercedNumber !== 0 && isFinite(coercedNumber)) { if (coercedNumber >= 0) { value = Math.floor(coercedNumber); } else { value = Math.ceil(coercedNumber); } } return value; } /* * Parses an HL7 name (prefix / given [] / family) */ var parseName = function (nameEl) { var prefix = nameEl.tag('prefix').val(); var els = nameEl.elsByTag('given'); var given = []; for (var i = 0; i < els.length; i++) { var val = els[i].val(); if (val) { given.push(val); } } var family = nameEl.tag('family').val(); return { prefix: prefix, given: given, family: family }; }; /* * Parses an HL7 address (streetAddressLine [], city, state, postalCode, country) */ var parseAddress = function (addrEl) { var els = addrEl.elsByTag('streetAddressLine'); var street = []; for (var i = 0; i < els.length; i++) { var val = els[i].val(); if (val) { street.push(val); } } var city = addrEl.tag('city').val(), state = addrEl.tag('state').val(), zip = addrEl.tag('postalCode').val(), country = addrEl.tag('country').val(); return { street: street, city: city, state: state, zip: zip, country: country }; }; // Node-specific code for unit tests if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { module.exports = { parseDate: parseDate }; } } return { detect: detect, entries: entries, parseDate: parseDate, parseName: parseName, parseAddress: parseAddress }; })(); ; /* * ... */ Documents.C32 = (function () { /* * Preprocesses the C32 document */ var process = function (c32) { c32.section = section; return c32; }; /* * Finds the section of a C32 document * * Usually we check first for the HITSP section ID and then for the HL7-CCD ID. */ var section = function (name) { var el, entries = Documents.entries; switch (name) { case 'document': return this.template('2.16.840.1.113883.3.88.11.32.1'); case 'allergies': el = this.template('2.16.840.1.113883.3.88.11.83.102'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.1.2'); } el.entries = entries; return el; case 'demographics': return this.template('2.16.840.1.113883.3.88.11.32.1'); case 'encounters': el = this.template('2.16.840.1.113883.3.88.11.83.127'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.1.3'); } el.entries = entries; return el; case 'immunizations': el = this.template('2.16.840.1.113883.3.88.11.83.117'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.1.6'); } el.entries = entries; return el; case 'results': el = this.template('2.16.840.1.113883.3.88.11.83.122'); el.entries = entries; return el; case 'medications': el = this.template('2.16.840.1.113883.3.88.11.83.112'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.1.8'); } el.entries = entries; return el; case 'problems': el = this.template('2.16.840.1.113883.3.88.11.83.103'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.1.11'); } el.entries = entries; return el; case 'procedures': el = this.template('2.16.840.1.113883.3.88.11.83.108'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.1.12'); } el.entries = entries; return el; case 'vitals': el = this.template('2.16.840.1.113883.3.88.11.83.119'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.1.16'); } el.entries = entries; return el; } return null; }; return { process: process, section: section }; })(); ; /* * ... */ Documents.CCDA = (function () { /* * Preprocesses the CCDA document */ var process = function (ccda) { ccda.section = section; return ccda; }; /* * Finds the section of a CCDA document */ var section = function (name) { var el, entries = Documents.entries; switch (name) { case 'document': return this.template('2.16.840.1.113883.10.20.22.1.1'); case 'allergies': el = this.template('2.16.840.1.113883.10.20.22.2.6.1'); el.entries = entries; return el; case 'care_plan': el = this.template('2.16.840.1.113883.10.20.22.2.10'); el.entries = entries; return el; case 'chief_complaint': el = this.template('2.16.840.1.113883.10.20.22.2.13'); if (el.isEmpty()) { el = this.template('1.3.6.1.4.1.19376.1.5.3.1.1.13.2.1'); } // no entries in Chief Complaint return el; case 'demographics': return this.template('2.16.840.1.113883.10.20.22.1.1'); case 'encounters': el = this.template('2.16.840.1.113883.10.20.22.2.22'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.22.2.22.1'); } el.entries = entries; return el; case 'functional_statuses': el = this.template('2.16.840.1.113883.10.20.22.2.14'); el.entries = entries; return el; case 'immunizations': el = this.template('2.16.840.1.113883.10.20.22.2.2.1'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.22.2.2'); } el.entries = entries; return el; case 'instructions': el = this.template('2.16.840.1.113883.10.20.22.2.45'); el.entries = entries; return el; case 'results': el = this.template('2.16.840.1.113883.10.20.22.2.3.1'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.22.2.3'); } el.entries = entries; return el; case 'medications': el = this.template('2.16.840.1.113883.10.20.22.2.1.1'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.22.2.1'); } el.entries = entries; return el; case 'problems': el = this.template('2.16.840.1.113883.10.20.22.2.5.1'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.22.2.5'); } el.entries = entries; return el; case 'procedures': el = this.template('2.16.840.1.113883.10.20.22.2.7.1'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.22.2.7'); } el.entries = entries; return el; case 'social_history': el = this.template('2.16.840.1.113883.10.20.22.2.17'); el.entries = entries; return el; case 'vitals': el = this.template('2.16.840.1.113883.10.20.22.2.4.1'); if (el.isEmpty()) { el = this.template('2.16.840.1.113883.10.20.22.2.4'); } el.entries = entries; return el; } return null; }; return { process: process, section: section }; })(); ; /* * ... */ /* exported Generators */ var Generators = (function () { var method = function () {}; /* Import ejs if we're in Node. Then setup custom formatting filters */ if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { ejs = require("ejs"); } } if (typeof ejs !== 'undefined') { /* Filters are automatically available to ejs to be used like "... | hl7Date" * Helpers are functions that we'll manually pass in to ejs. * The intended distinction is that a helper gets called with regular function-call syntax */ var pad = function(number) { if (number < 10) { return '0' + number; } return String(number); }; ejs.filters.hl7Date = function(obj) { try { if (obj === null || obj === undefined) { return 'nullFlavor="UNK"'; } var date = new Date(obj); if (isNaN(date.getTime())) { return obj; } var dateStr = null; if (date.getHours() || date.getMinutes() || date.getSeconds()) { // If there's a meaningful time, output a UTC datetime dateStr = date.getUTCFullYear() + pad( date.getUTCMonth() + 1 ) + pad( date.getUTCDate() ); var timeStr = pad( date.getUTCHours() ) + pad( date.getUTCMinutes() ) + pad ( date.getUTCSeconds() ) + "+0000"; return 'value="' + dateStr + timeStr + '"'; } else { // If there's no time, don't apply timezone tranformations: just output a date dateStr = String(date.getFullYear()) + pad( date.getMonth() + 1 ) + pad( date.getDate() ); return 'value="' + dateStr + '"'; } } catch (e) { return obj; } }; var escapeSpecialChars = function(s) { return s.replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/&/g, '&amp;') .replace(/"/g, '&quot;') .replace(/'/g, '&apos;'); }; ejs.filters.hl7Code = function(obj) { if (!obj) { return ''; } var tag = ''; var name = obj.name || ''; if (obj.name) { tag += 'displayName="'+escapeSpecialChars(name)+'"'; } if (obj.code) { tag += ' code="'+obj.code+'"'; if (obj.code_system) { tag += ' codeSystem="'+escapeSpecialChars(obj.code_system)+'"'; } if (obj.code_system_name) { tag += ' codeSystemName="' + escapeSpecialChars(obj.code_system_name)+'"'; } } else { tag += ' nullFlavor="UNK"'; } if (!obj.name && ! obj.code) { return 'nullFlavor="UNK"'; } return tag; }; ejs.filters.emptyStringIfFalsy = function(obj) { if (!obj) { return ''; } return obj; }; if (!ejs.helpers) ejs.helpers = {}; ejs.helpers.simpleTag = function(tagName, value) { if (value) { return "<"+tagName+">"+value+"</"+tagName+">"; } else { return "<"+tagName+" nullFlavor=\"UNK\" />"; } }; ejs.helpers.addressTags = function(addressDict) { if (!addressDict) { return '<streetAddressLine nullFlavor="NI" />\n' + '<city nullFlavor="NI" />\n' + '<state nullFlavor="NI" />\n' + '<postalCode nullFlavor="NI" />\n' + '<country nullFlavor="NI" />\n'; } var tags = ''; if (!addressDict.street.length) { tags += ejs.helpers.simpleTag('streetAddressLine', null) + '\n'; } else { for (var i=0; i<addressDict.street.length; i++) { tags += ejs.helpers.simpleTag('streetAddressLine', addressDict.street[i]) + '\n'; } } tags += ejs.helpers.simpleTag('city', addressDict.city) + '\n'; tags += ejs.helpers.simpleTag('state', addressDict.state) + '\n'; tags += ejs.helpers.simpleTag('postalCode', addressDict.zip) + '\n'; tags += ejs.helpers.simpleTag('country', addressDict.country) + '\n'; return tags; }; ejs.helpers.nameTags = function(nameDict) { if (!nameDict) { return '<given nullFlavor="NI" />\n' + '<family nullFlavor="NI" />\n'; } var tags = ''; if (nameDict.prefix) { tags += ejs.helpers.simpleTag('prefix', nameDict.prefix) + '\n'; } if (!nameDict.given.length) { tags += ejs.helpers.simpleTag('given', null) + '\n'; } else { for (var i=0; i<nameDict.given.length; i++) { tags += ejs.helpers.simpleTag('given', nameDict.given[i]) + '\n'; } } tags += ejs.helpers.simpleTag('family', nameDict.family) + '\n'; if (nameDict.suffix) { tags += ejs.helpers.simpleTag('suffix', nameDict.suffix) + '\n'; } return tags; }; } return { method: method }; })(); ; /* * ... */ Generators.C32 = (function () { /* * Generates a C32 document */ var run = function (json, template, testingMode) { /* jshint unused: false */ // only until this stub is actually implemented console.log("C32 generation is not implemented yet"); return null; }; return { run: run }; })(); ; /* * ... */ Generators.CCDA = (function () { /* * Generates a CCDA document * A lot of the EJS setup happens in generators.js * * If `testingMode` is true, we'll set the "now" variable to a specific, * fixed time, so that the expected XML doesn't change across runs */ var run = function (json, template, testingMode) { if (!ejs) { console.log("The BB.js Generator (JSON->XML) requires the EJS template package. " + "Install it in Node or include it before this package in the browser."); return null; } if (!template) { console.log("Please provide a template EJS file for the Generator to use. " + "Load it via fs.readFileSync in Node or XHR in the browser."); return null; } // `now` is actually now, unless we're running this for a test, // in which case it's always Jan 1, 2000 at 12PM UTC var now = (testingMode) ? new Date('2000-01-01T12:00:00Z') : new Date(); var ccda = ejs.render(template, { filename: 'ccda.xml', bb: json, now: now, tagHelpers: ejs.helpers, codes: Core.Codes }); return ccda; }; return { run: run }; })(); ; /* * ... */ /* exported Parsers */ var Parsers = (function () { var method = function () {}; return { method: method }; })(); ; /* * Parser for the C32 document */ Parsers.C32 = (function () { var run = function (c32) { var data = {}; data.document = Parsers.C32.document(c32); data.allergies = Parsers.C32.allergies(c32); data.demographics = Parsers.C32.demographics(c32); data.encounters = Parsers.C32.encounters(c32); data.immunizations = Parsers.C32.immunizations(c32).administered; data.immunization_declines = Parsers.C32.immunizations(c32).declined; data.results = Parsers.C32.results(c32); data.medications = Parsers.C32.medications(c32); data.problems = Parsers.C32.problems(c32); data.procedures = Parsers.C32.procedures(c32); data.vitals = Parsers.C32.vitals(c32); data.json = Core.json; data.document.json = Core.json; data.allergies.json = Core.json; data.demographics.json = Core.json; data.encount