/**
  *
  */
 private static function _processChildren(SearchResult $pr_res, $po_nodes, array $pa_vals, array $pa_options = null)
 {
     if (!is_array($pa_options)) {
         $pa_options = [];
     }
     if (!$po_nodes) {
         return '';
     }
     $vs_acc = '';
     $ps_tablename = $pr_res->tableName();
     $o_dm = Datamodel::load();
     $t_instance = $o_dm->getInstanceByTableName($ps_tablename, true);
     $ps_delimiter = caGetOption('delimiter', $pa_options, '; ');
     $pb_is_case = caGetOption('isCase', $pa_options, false, ['castTo' => 'boolean']);
     $pb_quote = caGetOption('quote', $pa_options, false, ['castTo' => 'boolean']);
     $pa_primary_ids = caGetOption('primaryIDs', $pa_options, null);
     $pb_include_blanks = caGetOption('includeBlankValuesInArray', $pa_options, false);
     unset($pa_options['quote']);
     $vn_last_unit_omit_count = null;
     foreach ($po_nodes as $vn_index => $o_node) {
         switch ($vs_tag = strtolower($o_node->tag)) {
             case 'case':
                 if (!$pb_is_case) {
                     $vs_acc .= DisplayTemplateParser::_processChildren($pr_res, $o_node->children, $pa_vals, array_merge($pa_options, ['isCase' => true]));
                 }
                 break;
             case 'if':
                 if (strlen($vs_rule = $o_node->rule) && ExpressionParser::evaluate($vs_rule, $pa_vals)) {
                     $vs_acc .= DisplayTemplateParser::_processChildren($pr_res, $o_node->children, DisplayTemplateParser::_getValues($pr_res, DisplayTemplateParser::_getTags($o_node->children, $pa_options), $pa_options), $pa_options);
                     if ($pb_is_case) {
                         break 2;
                     }
                 }
                 break;
             case 'ifdef':
             case 'ifnotdef':
                 $vb_defined = DisplayTemplateParser::_evaluateCodeAttribute($pr_res, $o_node, ['index' => caGetOption('index', $pa_options, null), 'mode' => $vs_tag == 'ifdef' ? 'present' : 'not_present']);
                 if ($vs_tag == 'ifdef' && $vb_defined || $vs_tag == 'ifnotdef' && $vb_defined) {
                     // Make sure returned values are not empty
                     $vs_acc .= DisplayTemplateParser::_processChildren($pr_res, $o_node->children, DisplayTemplateParser::_getValues($pr_res, DisplayTemplateParser::_getTags($o_node->children, $pa_options), $pa_options), $pa_options);
                     if ($pb_is_case) {
                         break 2;
                     }
                 }
                 break;
             case 'ifcount':
                 $vn_min = (int) $o_node->min;
                 $vn_max = (int) $o_node->max;
                 if (!is_array($va_codes = DisplayTemplateParser::_getCodesFromAttribute($o_node)) || !sizeof($va_codes)) {
                     break;
                 }
                 $pa_check_access = $t_instance->hasField('access') ? caGetOption('checkAccess', $pa_options, null) : null;
                 if (!is_array($pa_check_access) || !sizeof($pa_check_access)) {
                     $pa_check_access = null;
                 }
                 $vb_bool = DisplayTemplateParser::_getCodesBooleanModeAttribute($o_node);
                 $va_restrict_to_types = DisplayTemplateParser::_getCodesFromAttribute($o_node, ['attribute' => 'restrictToTypes']);
                 $va_exclude_types = DisplayTemplateParser::_getCodesFromAttribute($o_node, ['attribute' => 'excludeTypes']);
                 $va_restrict_to_relationship_types = DisplayTemplateParser::_getCodesFromAttribute($o_node, ['attribute' => 'restrictToRelationshipTypes']);
                 $va_exclude_to_relationship_types = DisplayTemplateParser::_getCodesFromAttribute($o_node, ['attribute' => 'excludeRelationshipTypes']);
                 $vm_count = $vb_bool == 'AND' ? 0 : [];
                 foreach ($va_codes as $vs_code) {
                     $va_vals = $pr_res->get($vs_code, ['checkAccess' => $pa_check_access, 'returnAsArray' => true, 'restrictToTypes' => $va_restrict_to_types, 'excludeTypes' => $va_exclude_types, 'restrictToRelationshipTypes' => $va_restrict_to_relationship_types, 'excludeRelationshipTypes' => $va_exclude_to_relationship_types]);
                     if (is_array($va_vals)) {
                         if ($vb_bool == 'AND') {
                             $vm_count += sizeof($va_vals);
                         } else {
                             $vm_count[$vs_code] = sizeof($va_vals);
                         }
                     }
                 }
                 if ($vb_bool == 'AND') {
                     if ($vn_min <= $vm_count && ($vn_max >= $vm_count || !$vn_max)) {
                         $vs_acc .= DisplayTemplateParser::_processChildren($pr_res, $o_node->children, DisplayTemplateParser::_getValues($pr_res, DisplayTemplateParser::_getTags($o_node->children, $pa_options), $pa_options), $pa_options);
                         if ($pb_is_case) {
                             break 2;
                         }
                     }
                 } else {
                     $vb_all_have_count = true;
                     foreach ($vm_count as $vs_code => $vn_count) {
                         if (!($vn_min <= $vn_count && ($vn_max >= $vn_count || !$vn_max))) {
                             $vb_all_have_count = false;
                             break 2;
                         }
                     }
                     if ($vb_all_have_count) {
                         $vs_acc .= DisplayTemplateParser::_processChildren($pr_res, $o_node->children, $pa_vals, $pa_options);
                         if ($pb_is_case) {
                             break 2;
                         }
                     }
                 }
                 break;
             case 'more':
                 // Does a placeholder with value follow this tag?
                 // NOTE: 	We don't take into account <ifdef> and friends when finding a set placeholder; it may be set but not visible due to a conditional
                 // 			This case is not covered at the moment on the assumption that if you're using <more> you're not using conditionals. This may or may not be a good assumption.
                 for ($vn_i = $vn_index + 1; $vn_i < sizeof($po_nodes); $vn_i++) {
                     if ($po_nodes[$vn_i] && $po_nodes[$vn_i]->tag == '~text~' && is_array($va_following_tags = caGetTemplateTags($po_nodes[$vn_i]->text))) {
                         foreach ($va_following_tags as $vs_following_tag) {
                             if (isset($pa_vals[$vs_following_tag]) && strlen($pa_vals[$vs_following_tag])) {
                                 $vs_acc .= DisplayTemplateParser::_processChildren($pr_res, $o_node->children, $pa_vals, $pa_options);
                                 if ($pb_is_case) {
                                     break 2;
                                 }
                             }
                         }
                     }
                 }
                 break;
             case 'between':
                 // Does a placeholder with value precede this tag?
                 // NOTE: 	We don't take into account <ifdef> and friends when finding a set placeholder; it may be set but not visible due to a conditional
                 // 			This case is not covered at the moment on the assumption that if you're using <between> you're not using conditionals. This may or may not be a good assumption.
                 $vb_has_preceding_value = false;
                 for ($vn_i = 0; $vn_i < $vn_index; $vn_i++) {
                     if ($po_nodes[$vn_i] && $po_nodes[$vn_i]->tag == '~text~' && is_array($va_preceding_tags = caGetTemplateTags($po_nodes[$vn_i]->text))) {
                         foreach ($va_preceding_tags as $vs_preceding_tag) {
                             if (isset($pa_vals[$vs_preceding_tag]) && strlen($pa_vals[$vs_preceding_tag])) {
                                 $vb_has_preceding_value = true;
                             }
                         }
                     }
                 }
                 if ($vb_has_preceding_value) {
                     // Does it have a value immediately following it?
                     for ($vn_i = $vn_index + 1; $vn_i < sizeof($po_nodes); $vn_i++) {
                         if ($po_nodes[$vn_i] && $po_nodes[$vn_i]->tag == '~text~' && is_array($va_following_tags = caGetTemplateTags($po_nodes[$vn_i]->text))) {
                             foreach ($va_following_tags as $vs_following_tag) {
                                 if (isset($pa_vals[$vs_following_tag]) && strlen($pa_vals[$vs_following_tag])) {
                                     $vs_acc .= DisplayTemplateParser::_processChildren($pr_res, $o_node->children, $pa_vals, $pa_options);
                                     if ($pb_is_case) {
                                         break 2;
                                     }
                                 }
                                 break;
                             }
                         }
                     }
                 }
                 break;
             case 'expression':
                 if ($vs_exp = trim($o_node->getInnerText())) {
                     $vs_acc .= ExpressionParser::evaluate(DisplayTemplateParser::_processChildren($pr_res, $o_node->children, DisplayTemplateParser::_getValues($pr_res, DisplayTemplateParser::_getTags($o_node->children, $pa_options), $pa_options), array_merge($pa_options, ['quote' => true])), $pa_vals);
                     if ($pb_is_case) {
                         break 2;
                     }
                 }
                 break;
             case 'unit':
                 $va_relative_to_tmp = $o_node->relativeTo ? explode(".", $o_node->relativeTo) : [$ps_tablename];
                 if ($va_relative_to_tmp[0] && !($t_rel_instance = $o_dm->getInstanceByTableName($va_relative_to_tmp[0], true))) {
                     continue;
                 }
                 $vn_last_unit_omit_count = 0;
                 // <unit> attributes
                 $vs_unit_delimiter = $o_node->delimiter ? (string) $o_node->delimiter : $ps_delimiter;
                 $vb_unique = $o_node->unique ? (bool) $o_node->unique : false;
                 $vb_aggregate_unique = $o_node->aggregateUnique ? (bool) $o_node->aggregateUnique : false;
                 $vs_unit_skip_if_expression = (string) $o_node->skipIfExpression;
                 $vn_start = (int) $o_node->start;
                 $vn_length = (int) $o_node->length;
                 $pa_check_access = $t_instance->hasField('access') ? caGetOption('checkAccess', $pa_options, null) : null;
                 if (!is_array($pa_check_access) || !sizeof($pa_check_access)) {
                     $pa_check_access = null;
                 }
                 // additional get options for pulling related records
                 $va_get_options = ['returnAsArray' => true, 'checkAccess' => $pa_check_access];
                 $va_get_options['restrictToTypes'] = DisplayTemplateParser::_getCodesFromAttribute($o_node, ['attribute' => 'restrictToTypes']);
                 $va_get_options['excludeTypes'] = DisplayTemplateParser::_getCodesFromAttribute($o_node, ['attribute' => 'excludeTypes']);
                 $va_get_options['restrictToRelationshipTypes'] = DisplayTemplateParser::_getCodesFromAttribute($o_node, ['attribute' => 'restrictToRelationshipTypes']);
                 $va_get_options['excludeRelationshipTypes'] = DisplayTemplateParser::_getCodesFromAttribute($o_node, ['attribute' => 'excludeRelationshipTypes']);
                 if ($o_node->sort) {
                     $va_get_options['sort'] = preg_split('![ ,;]+!', $o_node->sort);
                     $va_get_options['sortDirection'] = $o_node->sortDirection;
                 }
                 if (sizeof($va_relative_to_tmp) == 1 && $va_relative_to_tmp[0] == $ps_tablename || sizeof($va_relative_to_tmp) >= 1 && $va_relative_to_tmp[0] == $ps_tablename && $va_relative_to_tmp[1] != 'related') {
                     $vs_relative_to_container = null;
                     switch (strtolower($va_relative_to_tmp[1])) {
                         case 'hierarchy':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".hierarchy." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         case 'parent':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".parent." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         case 'children':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".children." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         case 'siblings':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".siblings." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         default:
                             // If relativeTo is not set to a valid attribute try to guess from template
                             if ($t_rel_instance->isValidMetadataElement(join(".", array_slice($va_relative_to_tmp, 1, 1)), true)) {
                                 $vs_relative_to_container = join(".", array_slice($va_relative_to_tmp, 0, 2));
                             } else {
                                 $va_tags = caGetTemplateTags($o_node->getInnerText());
                                 foreach ($va_tags as $vs_tag) {
                                     $va_tag = explode('.', $vs_tag);
                                     while (sizeof($va_tag) > 1) {
                                         $vs_end = array_pop($va_tag);
                                         if ($t_rel_instance->isValidMetadataElement($vs_end, true)) {
                                             $va_tag[] = $vs_end;
                                             $vs_relative_to_container = join(".", $va_tag);
                                             break 2;
                                         }
                                     }
                                 }
                             }
                             $va_relative_ids = array($pr_res->getPrimaryKey());
                             break;
                     }
                     // process template for all records selected by unit tag
                     $va_tmpl_val = DisplayTemplateParser::evaluate($o_node->getInnerText(), $ps_tablename, $va_relative_ids, array_merge($pa_options, ['sort' => $va_get_options['sort'], 'sortDirection' => $va_get_options['sortDirection'], 'returnAsArray' => true, 'delimiter' => $vs_unit_delimiter, 'skipIfExpression' => $vs_unit_skip_if_expression, 'placeholderPrefix' => (string) $o_node->relativeTo, 'restrictToTypes' => $va_get_options['restrictToTypes'], 'excludeTypes' => $va_get_options['excludeTypes'], 'isUnit' => true, 'unitStart' => $vn_start, 'unitLength' => $vn_length, 'relativeToContainer' => $vs_relative_to_container, 'includeBlankValuesInTopLevelForPrefetch' => false, 'unique' => $vb_unique, 'aggregateUnique' => $vb_aggregate_unique]));
                     if ($vb_unique) {
                         $va_tmpl_val = array_unique($va_tmpl_val);
                     }
                     if ($vn_start > 0 || !is_null($vn_length)) {
                         $vn_last_unit_omit_count = sizeof($va_tmpl_val) - ($vn_length - $vn_start);
                     }
                     if (caGetOption('returnAsArray', $pa_options, false)) {
                         return $va_tmpl_val;
                     }
                     $vs_acc .= join($vs_unit_delimiter, $va_tmpl_val);
                     if ($pb_is_case) {
                         break 2;
                     }
                 } else {
                     switch (strtolower($va_relative_to_tmp[1])) {
                         case 'hierarchy':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".hierarchy." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         case 'parent':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".parent." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         case 'children':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".children." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         case 'siblings':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".siblings." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         case 'related':
                             $va_relative_ids = $pr_res->get($t_rel_instance->tableName() . ".related." . $t_rel_instance->primaryKey(), $va_get_options);
                             $va_relative_ids = array_values($va_relative_ids);
                             break;
                         default:
                             if (method_exists($t_instance, 'isSelfRelationship') && $t_instance->isSelfRelationship() && is_array($pa_primary_ids) && isset($pa_primary_ids[$t_rel_instance->tableName()])) {
                                 $va_relative_ids = array_values($t_instance->getRelatedIDsForSelfRelationship($pa_primary_ids[$t_rel_instance->tableName()], array($pr_res->getPrimaryKey())));
                             } else {
                                 $va_relative_ids = $pr_res->get($t_rel_instance->primaryKey(true), $va_get_options);
                                 $va_relative_ids = is_array($va_relative_ids) ? array_values($va_relative_ids) : array();
                             }
                             break;
                     }
                     $va_tmpl_val = DisplayTemplateParser::evaluate($o_node->getInnerText(), $va_relative_to_tmp[0], $va_relative_ids, array_merge($pa_options, ['sort' => $va_unit['sort'], 'sortDirection' => $va_unit['sortDirection'], 'delimiter' => $vs_unit_delimiter, 'returnAsArray' => true, 'skipIfExpression' => $vs_unit_skip_if_expression, 'placeholderPrefix' => (string) $o_node->relativeTo, 'restrictToTypes' => $va_get_options['restrictToTypes'], 'excludeTypes' => $va_get_options['excludeTypes'], 'isUnit' => true, 'unitStart' => $vn_start, 'unitLength' => $vn_length, 'includeBlankValuesInTopLevelForPrefetch' => false, 'unique' => $vb_unique, 'aggregateUnique' => $vb_aggregate_unique]));
                     if ($vb_unique) {
                         $va_tmpl_val = array_unique($va_tmpl_val);
                     }
                     if ($vn_start > 0 || !is_null($vn_length)) {
                         $vn_num_vals = sizeof($va_tmpl_val);
                         $va_tmpl_val = array_slice($va_tmpl_val, $vn_start, $vn_length > 0 ? $vn_length : null);
                         $vn_last_unit_omit_count = $vn_num_vals - ($vn_length - $vn_start);
                     }
                     if (caGetOption('returnAsArray', $pa_options, false)) {
                         return $va_tmpl_val;
                     }
                     $vs_acc .= join($vs_unit_delimiter, $va_tmpl_val);
                     if ($pb_is_case) {
                         break 2;
                     }
                 }
                 break;
             case 'whenunitomits':
                 if ($vn_last_unit_omit_count > 0) {
                     $vs_proc_template = caProcessTemplate($o_node->getInnerText(), array_merge($pa_vals, ['omitcount' => (int) $vn_last_unit_omit_count]), ['quote' => $pb_quote]);
                     $vs_acc .= $vs_proc_template;
                 }
                 break;
             default:
                 if ($o_node->children && sizeof($o_node->children) > 0) {
                     $vs_proc_template = DisplayTemplateParser::_processChildren($pr_res, $o_node->children, $pa_vals, $pa_options);
                 } else {
                     $vs_proc_template = caProcessTemplate($o_node->html(), $pa_vals, ['quote' => $pb_quote]);
                 }
                 if ($vs_tag === 'l') {
                     $va_proc_templates = caCreateLinksFromText(["{$vs_proc_template}"], $ps_tablename, [$pr_res->getPrimaryKey()], null, caGetOption('linkTarget', $pa_options, null), array_merge(['addRelParameter' => true, 'requireLinkTags' => false], $pa_options));
                     $vs_proc_template = array_shift($va_proc_templates);
                 } elseif (strlen($vs_tag) && $vs_tag[0] !== '~') {
                     if ($o_node->children && sizeof($o_node->children) > 0) {
                         $vs_attr = '';
                         if ($o_node->attributes) {
                             foreach ($o_node->attributes as $attribute => $value) {
                                 $vs_attr .= " {$attribute}=\"" . htmlspecialchars(caProcessTemplate($value, $pa_vals, ['quote' => $pb_quote])) . "\"";
                             }
                         }
                         $vs_proc_template = "<{$vs_tag}{$vs_attr}>{$vs_proc_template}</{$vs_tag}>";
                     } elseif ($o_node->attributes && sizeof($o_node->attributes) > 0) {
                         $vs_attr = '';
                         foreach ($o_node->attributes as $attribute => $value) {
                             $vs_attr .= " {$attribute}=\"" . htmlspecialchars(caProcessTemplate($value, $pa_vals, ['quote' => $pb_quote])) . "\"";
                         }
                         switch (strtolower($vs_tag)) {
                             case 'br':
                             case 'hr':
                             case 'meta':
                             case 'link':
                             case 'base':
                             case 'img':
                             case 'embed':
                             case 'param':
                             case 'area':
                             case 'col':
                             case 'input':
                                 $vs_proc_template = "<{$vs_tag}{$vs_attr} />";
                                 break;
                             default:
                                 $vs_proc_template = "<{$vs_tag}{$vs_attr}></{$vs_tag}>";
                                 break;
                         }
                     } else {
                         $vs_proc_template = $o_node->html();
                     }
                 }
                 $vs_acc .= $vs_proc_template;
                 break;
         }
     }
     return $vs_acc;
 }
 public function testAttributesWithHTML()
 {
     $vm_ret = DisplayTemplateParser::evaluate("<unit relativeTo='ca_entities' delimiter='<br/>'>^ca_entities.preferred_labels.displayname</unit>", "ca_objects", array($this->opn_object_id), array('returnAsArray' => true));
     $this->assertInternalType('array', $vm_ret);
     $this->assertCount(1, $vm_ret);
     $this->assertEquals("Homer J. Simpson<br/>Bart Simpson", $vm_ret[0]);
     $vm_ret = DisplayTemplateParser::evaluate("<unit relativeTo='ca_entities' delimiter='  <br/>  '>^ca_entities.preferred_labels.displayname</unit>", "ca_objects", array($this->opn_object_id), array('returnAsArray' => true));
     $this->assertInternalType('array', $vm_ret);
     $this->assertCount(1, $vm_ret);
     $this->assertEquals("Homer J. Simpson  <br/>  Bart Simpson", $vm_ret[0]);
 }
 public function testSiblingRestrictToTypesDisplayTemplates()
 {
     $vm_ret = DisplayTemplateParser::evaluate("<unit delimiter='; ' restrictToTypes='image' relativeTo='ca_objects.siblings'>^ca_objects.preferred_labels.name</unit>", "ca_objects", array($this->opt_another_child_object->getPrimaryKey()), array('returnAsArray' => true));
     $this->assertInternalType('array', $vm_ret);
     $this->assertCount(1, $vm_ret);
     $this->assertEquals('My test still; Another test still', $vm_ret[0]);
     $vm_ret = DisplayTemplateParser::evaluate("<unit delimiter='; ' restrictToTypes='moving_image' relativeTo='ca_objects.siblings'>^ca_objects.preferred_labels.name</unit>", "ca_objects", array($this->opt_another_child_object->getPrimaryKey()), array('returnAsArray' => true));
     $this->assertInternalType('array', $vm_ret);
     $this->assertCount(1, $vm_ret);
     $this->assertEquals('A child movie', $vm_ret[0]);
 }
/**
 * Replace "^" prefixed tags (eg. ^forename) in a template with values from an array
 *
 * @param string $ps_template String with embedded tags. Tags are just alphanumeric strings prefixed with a caret ("^")
 * @param string $pm_tablename_or_num Table name or number of table from which values are being formatted
 * @param string $pa_row_ids An array of primary key values in the specified table to be pulled into the template
 * @param array $pa_options Supported options are:
 *		returnAsArray = if true an array of processed template values is returned, otherwise the template values are returned as a string joined together with a delimiter. Default is false.
 *		delimiter = value to string together template values with when returnAsArray is false. Default is ';' (semicolon)
 *		placeholderPrefix = attribute container to implicitly place primary record fields into. Ex. if the table is "ca_entities" and the placeholder is "address" then tags like ^city will resolve to ca_entities.address.city
 *		requireLinkTags = if set then links are only added when explicitly defined with <l> tags. Default is to make the entire text a link in the absence of <l> tags.
 *		primaryIDs = row_ids for primary rows in related table, keyed by table name; when resolving ambiguous relationships the row_ids will be excluded from consideration. This option is rarely used and exists primarily to take care of a single
 *						edge case: you are processing a template relative to a self-relationship such as ca_entities_x_entities that includes references to the subject table (ca_entities, in the case of ca_entities_x_entities). There are
 *						two possible paths to take in this situations; primaryIDs lets you specify which ones you *don't* want to take by row_id. For interstitial editors, the ids will be set to a single id: that of the subject (Eg. ca_entities) row
 *						from which the interstitial was launched.
 *		sort = optional list of tag values to sort repeating values within a row template on. The tag must appear in the template. You can specify more than one tag by separating the tags with semicolons.
 *		sortDirection = The direction of the sort of repeating values within a row template. May be either ASC (ascending) or DESC (descending). [Default is ASC]
 *		linkTarget = Optional target to use when generating <l> tag-based links. By default links point to standard detail pages, but plugins may define linkTargets that point elsewhere.
 * 		skipIfExpression = skip the elements in $pa_row_ids for which the given expression does not evaluate true
 *		includeBlankValuesInArray = include blank template values in primary template and all <unit>s in returned array when returnAsArray is set. If you need the returned array of values to line up with the row_ids in $pa_row_ids this should be set. [Default is false]
 *		includeBlankValuesInTopLevelForPrefetch = include blank template values in *primary template* (not <unit>s) in returned array when returnAsArray is set. Used by template prefetcher to ensure returned values align with id indices. [Default is false]
 *
 * @return mixed Output of processed templates
 */
function caProcessTemplateForIDs($ps_template, $pm_tablename_or_num, $pa_row_ids, $pa_options = null)
{
    return DisplayTemplateParser::evaluate($ps_template, $pm_tablename_or_num, $pa_row_ids, $pa_options);
}