bluebutton
Version:
BlueButton.js helps developers navigate complex health data with ease.
1,701 lines (1,515 loc) • 116 kB
JavaScript
(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(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/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, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
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