Name

sn_cd.cd_ContentParser

Description

API to parse rich text and perform variable substitutions.

Script

var cd_ContentParser = Class.create();
cd_ContentParser.prototype = {
  initialize: function() {		
  	/*
  		REGEX:
  		    /                                -     begin regex
  			\${                              -     string starting with
  			([a-zA-Z0-9_.]+)                 -     field name can contain 1 (+ at the end) or more
  			( | | |\s|\t)*    -     zero or more combination of non-breaking space, en space, em space, space, or tab
  			(\-|\+){1}                       -     exactly one occurence of either (-) or (+)
  			( | | |\s|\t)*    -     zero or more combination of non-breaking space, en space, em space, space, or tab
  			(\d{1,4})                        -     1 to 4 number long co-efficient for the offset
  			([wdm]{1})                       -     exactly one occurence of w(week), d(day) or m(month)
  			( | | |\s|\t)*    -     zero or more combination of non-breaking space, en space, em space, space, or tab
  			}                                -     ending with
  			/                                -     end regex
  	*/	
  	this.ALL_FIELD_REGEX = /\${([a-zA-Z0-9_.]+)( | | |\s|\t)*([\-|\+]{1})( | | |\s|\t)*(\d{1,4})([wdm]{1})( | | |\s|\t)*}/;

  	/*
  		REGEX:
  		    /                                -     begin regex
  			\${                              -     string starting with
  			(Date)                           -     current date 'Date' field 
  			( | | |\s|\t)*    -     zero or more combination of non-breaking space, en space, em space, space, or tab
  			(\-|\+){1}                       -     exactly one occurence of either (-) or (+)
  			( | | |\s|\t)*    -     zero or more combination of non-breaking space, en space, em space, space, or tab
  			(\d{1,4})                        -     1 to 4 number long co-efficient for the offset
  			([wdm]{1})                       -     exactly one occurence of w(week), d(day) or m(month)
  			( | | |\s|\t)*    -     zero or more combination of non-breaking space, en space, em space, space, or tab
  			}                                -     ending with
  			/                                -     end regex
  	*/
  	this.CURRENT_DATE_REGEX = /\${(Date)( | | |\s|\t)*([\-|\+]{1})( | | |\s|\t)*(\d{1,4})([wdm]{1})( | | |\s|\t)*}/;
  	// Date regex with global flag
  	this.ALL_CURRENT_DATE_REGEX = new RegExp(this.CURRENT_DATE_REGEX.source, "g");

  	this.unEvaluatedVariable = [];
  	this.inaccessibleVariable = [];
  	this.customVariables = '${Date}';
  },

  /*
  Parser should support all below:
  	${Date}
  	${Date + 1d}
  	${Date + 1w}
  	${Date + 1m}
  	${Date - 1d}
  	${Date - 1w}
  	${Date - 1m}
  	${employment_start_date}
  	${employment_start_date - 52w}
  	${user.manager}
  	Requirements for Content Publishing:
  		Support above examples.
  		If field doesn't exist, no substitution should happen. For example, ${bogus_field} where bogus_field is a non-existent field name should return "${bogus_field}".
  		Additionally, UI should attempt to prevent. Current HR logic aborts update with message:  
  			Following variables are not valid or defined: ${bogus_field}. 
  		UX also highlights the bad field in red (is happening on save, not real time).
  		if no permissions, return blank
  		if there is no 'user' record to use:
  		For example, the user table is HR Profile, user field is 'user', but the logged in user has no HR Profile, then you'd expect to see like below:
  			Employment start date:
  			52 weeks prior to your start date is 
  			User Manager:
  */

  /* Validate rich text
  * @param parsedBody String Text to validate
  * @param tableName String Name of table to validate parsedBody with
  * @return String Parsed body
  */
  validateTemplate: function(parsedBody, tableName) {
  	if (tableName) {
  		parsedBody = this.resetErroredSpanInDocument(parsedBody);
  		parsedBody = this._convertEscapedPlusSymbol(parsedBody);
  		var grs = new GlideRecordSecure(tableName);
  		grs.initialize();
  		if (grs.isValid())
  			return this._doVariableSubstitutions(parsedBody, grs, true);	
  	}
  	
  	return parsedBody;
  },

  /* Parse a body of text by doing variable substitutions
  * @param docBody String String text to parse
  * @param grs GlideRecordSecure Record for a user or user reference table
  * @return String Parsed body
  */
  parseRichTextHtmlBody : function(docBody, grs) {
  	docBody = this._convertEscapedPlusSymbol(docBody);
  	docBody = this._doVariableSubstitutions(docBody, grs, false);
  	docBody = this._setDateValues(docBody);
  	return docBody;
  },

  /* Set date value substitutions in a string
  * @param docBody String Text to parse
  * return String Parsed body with date occurences replaced
  */
  _setDateValues: function(docBody) {
  	// Get offsetObject for each date occurence and replace with offset value 
  	var date = new GlideDateTime().getLocalDate();
  	var offsetCurrentDateOccurence = docBody.match(this.ALL_CURRENT_DATE_REGEX);
  	for (var index in offsetCurrentDateOccurence) {
  		var offsetObject = this._offsetValues(offsetCurrentDateOccurence[index]);
  		var dateWithOffset = this._applyOffset(date, offsetObject);
  		docBody = docBody.replace(offsetCurrentDateOccurence[index], dateWithOffset);
  	}

  	// Replace current date tag ${Date} with current date value
  	return docBody.replace(/\${Date\}/gi, date.getDisplayValue());
  },

  /* Runs through text body and checks every variable, replacing with the value they represent if initial check passes
  * @param parsedBody String String text to parse
  * @param gr GlideRecordSecure Record for a user or user reference table
  * @param validate boolean Whether to just validate (true), or actually do substitutions (false)
  * @return String Parsed body with variable occurences replaced where applicable
  */
  _doVariableSubstitutions: function (parsedBody, gr, validate) {
  	var regex = /\${([^}]*)}/g;
  	var matched = parsedBody.match(regex);
  	for (var i in matched) {
  		if (this.unEvaluatedVariable.indexOf(matched[i]) > -1  || this.inaccessibleVariable.indexOf(matched[i]) > -1 || this.customVariables.indexOf(matched[i]) > -1 || this._isCurrentDateWithOffset(matched[i])) 
  			continue;

  		//Date processing
  		var isOffsetDate = this._isOffsetDate(matched[i], gr);
  		var offsetData = this._offsetValues(matched[i]);
  		var str = isOffsetDate ? offsetData.reference.trim() : matched[i].match(/\${(.*)}/).pop().trim();
  		if (!str || !gr) {
  			parsedBody = parsedBody.replace(matched[i], '');
  			continue;
  		}

  		var ge = gr.getElement(str);
  		if (ge === null || ge.toString() === null) {
  			if (validate)
  				parsedBody = this._pushToInaccessible(parsedBody, matched[i], false);
  			else
  				parsedBody = parsedBody.replace(matched[i], '');
  		} else if (!ge.canRead()) {
  			if (validate)
  				parsedBody = this._pushToInaccessible(parsedBody, matched[i], true);
  			else
  				parsedBody = parsedBody.replace(matched[i], '');
  		} else {
  			if (validate)
  				continue;
  			else if (isOffsetDate)
  				parsedBody = parsedBody.replace(matched[i], this._applyOffset(ge.getDisplayValue(), offsetData));
  			else
  				parsedBody = parsedBody.replace(matched[i], GlideStringUtil.escapeHTML(ge.getDisplayValue()));
  		}
  	}

  	return parsedBody;
  },

  _isOffsetDate: function(field, glideRecSecure) {
  	var re = this.ALL_FIELD_REGEX;
  	var dateWithOffset = field.match(re);
  	var isDateType = false;
  	if (dateWithOffset != null && glideRecSecure)
  		isDateType = this._isDateType(dateWithOffset[1], glideRecSecure);

  	return (dateWithOffset != null && isDateType);
  },

  // Checks if an element is of known date types i.e. glide_date_time, date, glide_date
  _isDateType: function (fieldName, glideRecSecure) {
  	if (glideRecSecure.isValidField(fieldName)) {
  		var dateTypes = ['glide_date_time', 'date', 'glide_date', 'due_date'];
  		var element = glideRecSecure.getElement(fieldName);

  		return ((element.toString() != null) && dateTypes.indexOf(element.getED().getInternalType()) > -1);
  	}
  	return false;
  },

  _isCurrentDateWithOffset: function(field) {
  	var re = this.CURRENT_DATE_REGEX;
  	return (field.match(re) != null);
  },

  _offsetValues: function(field) {
  	var re = this.ALL_FIELD_REGEX;
  	var dateWithOffset = field.match(re);

  	if (dateWithOffset != null) {
  		return {
  			"reference" : dateWithOffset[1], //field name
  			"sign" : dateWithOffset[3], // add (+) or subtract (-) date
  			"quantity" : dateWithOffset[5],
  			"type" : dateWithOffset[6]
  		};
  	}
  	return {
  		"reference" : "", //field name
  		"sign" : "", // add (+) or subtract (-) date
  		"quantity" : "",
  		"type" : ""
  	};
  },

  _applyOffset : function(elementValue, offsetValue) {
  	if (elementValue && !this._isOffsetObjectEmpty(offsetValue)) {
  		var date = new GlideDateTime();
  		date.setDisplayValue(elementValue);
  		var sign = parseInt(offsetValue.sign + '1');
  		var offset = sign * offsetValue.quantity; 

  		switch (offsetValue.type) {
  			case "w" : date.addWeeksLocalTime(offset);
  				break;
  			case "m" : date.addMonthsLocalTime(offset);
  				break;
  			case "d" : date.addDaysLocalTime(offset);
  				break;
  			default : break;
  		}
  		return date.getLocalDate().getDisplayValue();
  	}
  	
  	return "";
  },

  _isOffsetObjectEmpty: function(offsetObject) {
  	return offsetObject == null || offsetObject.reference == "" || offsetObject.sign == "" || offsetObject.quantity == "" || offsetObject.type == ""; 
  },

  resetErroredSpanInDocument : function(documentBody) {
  	//regular expression that matches all the span pairs in the documentBody with class as errored-field	
  	var spanRegex = /<\s*span\s*class="errored-field".*?>/g;
  	var matchedSpanTags = documentBody.match(spanRegex);
  	for (var i in matchedSpanTags)
  		documentBody = documentBody.replace(matchedSpanTags[i],"<span>");

  	return documentBody;
  },

  // Convert an escaped plus symbol (&#43; -> +) to a plus symbol
  _convertEscapedPlusSymbol: function(parsedBody) {
  	parsedBody = parsedBody.replace(new RegExp('&#43;', 'g'), '+');
  	return parsedBody;
  },

  //Moves improper matched fields to inaccessible list and changes their text components to red
  _pushToInaccessible: function(docBody, matched, evaluated) {
  	docBody = docBody.replace(matched, '<span class="errored-field" style="color:#ff0000;">' + matched + '</span>');
  	if (evaluated)
  		this.inaccessibleVariable.push(matched);
  	else
  		this.unEvaluatedVariable.push(matched);

  	return docBody;
  },

  getUnevaluatedVariables: function() {
  	return this.unEvaluatedVariable;
  },

  getInaccessibleVariables: function() {
  	return this.inaccessibleVariable;
  },

  type: 'cd_ContentParser'
}; 

Sys ID

1c791e0753430300d901a7e6a11c08f9

Offical Documentation

Official Docs: