Schema Parser
/** | |
* @module schemaParser | |
* @namespace CN | |
* @description Handles the parsing of JSON from the MPP XML schemas into HTML. | |
* Allows for custom HTML templates to deliver the full rendered | |
* markup for a DCT, and also provides methods for accessing the | |
* data for various DCT elements. | |
* @requires CN | |
* @author Eric Shepherd | |
* @copyright 2008-2010 Conde Nast Digital | |
* | |
* @history: 0.8.0 EBS 12.2008 Alpha | |
* 0.9.0 EBS 02.2009 Beta - added support for memoizing renderedHtml() function with parameters, so the same content can be used with different templates on a page. | |
* 1.0.0 EBS 09.2009 Might as well make this production, it's been live for 6 months | |
* 1.1.0 EBS 10.2009 Added methods for date, contributor, expanded headers to allow graphics and URL | |
* 1.2.0 EBS 02.2010 Added methods for image annotations | |
* 1.3.0rc1 EBS 03.2010 Added methods for mediaItems | |
* 1.3.0rc2 EBS 03.2010 Removed need for templates (though no renderedHtml will be available) | |
* 1.3.0rc3 EBS 03.2010 Changed internal API for annotations to be a template for compatibility with both photo and mediaItems. | |
* 1.3.0rc4 EBS 04.2010 Added option to pass a content id to the parse() method if the json has already been parsed. | |
* 1.3.0 EBS 04.2010 Removed if check so that renderedHtml can be stored per template and memoized. | |
* 1.3.1 EBS 06.2010 Added passthru of templates from site js if it happens to load before this file. | |
* 1.3.2 EBS 07.2010 Bug fixes, reformatting | |
* | |
* @notes In general, the schema parser will not provide methods unless | |
* there is data for the method. That is, checking the presence | |
* of rubric method will tell you whether there is a rubric. | |
* The notable exception is photos, where the url, alt, etc | |
* methods are always written, and you can check url() to find | |
* out whether there is actual data, since it will return the | |
* falsy empty string if no data is entered. | |
*/ | |
/*global CN, console, window, document, jQuery, setTimeout, clearTimeout, clearInterval, setInterval */ /* for jsLint */ | |
/** | |
* Assists with converting JSON from DCTs into HTML for DOM insertion. | |
* | |
* @example CN.schemaParser.getInstance().parse(dct).rubric(); | |
* @example CN.schemaParser.getInstance().parse(dct).photo.main(); | |
* @example CN.schemaParser.getInstance().parse(dct).photo.main.annotations(); | |
* @example CN.schemaParser.getInstance().parse(dct).renderedHtml('item'); // where 'item' is the template to use | |
* | |
* @class schemaParser | |
* @singleton | |
* @param {Object} templates An object of templates to use for rendering (optional) - will be passed through and returned | |
* @return {Object} A getInstance() method and templates object (or null if param was not passed in) | |
*/ | |
CN.schemaParser = function(templates) { | |
var uniqueInstance, | |
constructor, | |
methods = {}, | |
data = {}; | |
constructor = function() { | |
return { | |
/** | |
* Stores the json data by contentId lookup | |
* @property data | |
* @static | |
*/ | |
data : data, | |
/** | |
* Parses a DCT, stores methods for accessing data in a dictionary | |
* keyed to content ID. Pass either a DCT (json at the article, | |
* list, or other main content level) or a contentId number for | |
* looking up an already processed DCT. | |
* | |
* @method parse | |
* @static | |
* @param {Object|Number} dct A dct in json format, or a content id for a dct previously parsed. | |
* @return {Object|False} Methods for accessing DCT content or false if dct param is a number but that piece of content hasn't been parsed. | |
*/ | |
parse : function(dct) { | |
var dctId = 'i' + ((CN.isNumber(dct)) ? dct : dct.metaData.id); | |
// Public catalog of content ids for implementation to query | |
if (!data[dctId]) { | |
if (CN.isNumber(dct)) { | |
return false; | |
} else { | |
data[dctId] = dct; | |
methods[dctId] = CN.schemaParser.factory(dct); | |
} | |
} else { | |
return methods[dctId]; | |
} | |
// Creates the renderedHtml() method and assigns to this piece of content | |
methods[dctId].renderedHtml = function(template) { | |
template = template || 'item'; | |
if (CN.schemaParser.templates && CN.schemaParser.templates[template]) { | |
return CN.schemaParser.templates[template](methods[dctId]); | |
} else { | |
return 'No renderedHtml template is available for the "' + template + '" template.'; | |
} | |
}; | |
// Memoize for future use | |
methods[dctId].renderedHtml = methods[dctId].renderedHtml.memoize(); | |
return methods[dctId]; | |
} | |
}; | |
}; | |
return { | |
getInstance : function() { | |
if (!uniqueInstance) { | |
uniqueInstance = constructor(); | |
} | |
return uniqueInstance; | |
}, | |
/** | |
* Passes through templates if passed in by being created at the site | |
* level before this file is loaded. Otherwise, the value will be | |
* null and this will remain null until the implementation overrides | |
* with its template object. | |
* @property templates | |
* @static | |
*/ | |
templates : templates | |
}; | |
}( | |
// Passing in templates object if it exists (in the case that | |
// this file lazy loads after the site sets up templates) | |
(CN.schemaParser && CN.schemaParser.templates) ? | |
CN.schemaParser.templates : | |
null | |
); | |
/** | |
* Generates methods to pass back to the user which will return DCT content. | |
* This will loop once for each piece of content with an ID. | |
* It looks at the top level elements in the DCT, and farms out to those | |
* methods. | |
* | |
* @method factory | |
* @static | |
* @param {Object} dct A DCT in JSON format to parse (at top level of content, | |
* such as article_v2, list, item, or video) | |
* @return {Object} A set of methods for accessing the DCT data | |
*/ | |
CN.schemaParser.factory = function(dct) { | |
var methods = {}, | |
i; | |
for (i in dct) { | |
if (CN.schemaParser.schemas.hasOwnProperty(i)) { | |
methods = CN.schemaParser.schemas[i](dct, methods); | |
} | |
} | |
return methods; | |
}; | |
/** | |
* DCT nodes which are called directly or used by other node methods. | |
* Each function must return the methods object after adding its own | |
* methods into that object. | |
* | |
* @property schemas | |
* @static | |
*/ | |
CN.schemaParser.schemas = { | |
/** | |
* Provides a text path to a Teamsite file for internal editing. No longer used. | |
* @deprecated | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
filePath : function(dct, methods) { | |
methods.filePath = function() { | |
return '<a class="editlink" target="_blank" href="http://' + CN.internal.getTeamsiteServer() + '/iw-cc/command/iw.group.ccpro.edit?vpath=' + dct.filePath.replace(/\/home/, '//' + CN.internal.getTeamsiteServer()).replace(/iwmnt-/, '') + '">Edit in TeamSite</a>'; | |
}; | |
return methods; | |
}, | |
/** | |
* Provides text for metaData information | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
metaData : function(dct, methods) { | |
methods.pageType = function() { | |
return dct.metaData.pageType; | |
}; | |
methods.metaDataItems = function() { | |
var results = [], | |
mediaItems = dct.metaData.properties && dct.metaData.properties.property ? dct.metaData.properties.property : []; | |
if (Object.prototype.toString.call(mediaItems) === '[object Array]') { | |
for (var ind = 0; ind < mediaItems.length; ind++) { | |
results[ind] = mediaItems[ind]; | |
} | |
} else { | |
results[0] = mediaItems; | |
} | |
return results; | |
}; | |
return methods; | |
}, | |
/** | |
* Delegates to other methods for unitMetaData information | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
unitMetaData : function(dct, methods) { | |
if (dct.unitMetaData.rubric) { | |
methods = this.rubric(dct.unitMetaData, methods); | |
} | |
if (dct.unitMetaData.byline) { | |
methods = this.byline(dct.unitMetaData, methods); | |
} | |
if (dct.unitMetaData.displayDate) { | |
methods = this.displayDate(dct.unitMetaData, methods); | |
} | |
return methods; | |
}, | |
/** | |
* Provides html text for header information | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
header : function(dct, methods) { | |
if (dct.header) { | |
var ret = '', | |
alt = '', | |
img = ''; | |
if (dct.header.graphic) { | |
alt = dct.header.graphic.altText || ''; | |
img = '<img src="' + dct.header.graphic.image + '" alt="' + alt + '" />'; | |
if (dct.header.graphic.URL) { | |
ret = '<a href="' + dct.header.graphic.URL + '">' + img + '</a>'; | |
} else { | |
ret = img; | |
} | |
} else { | |
if (dct.header.html.URL) { | |
ret += '<a href="' + dct.header.html.URL + '">' + dct.header.html.text + '</a>'; | |
} else { | |
ret += dct.header.html.text; | |
} | |
} | |
methods.header = function() { return ret; }; | |
} | |
return methods; | |
}, | |
/** | |
* Provides html text for subheader information | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
subHeaders : function(dct, methods) { | |
methods.subHeaders = function() { | |
var i, | |
subheaders=[]; | |
if(dct.subHeaders && jQuery.isArray(dct.subHeaders.subHeader)) { | |
for(i=0;i<dct.subHeaders.subHeader.length;i++) { | |
subheaders.push(dct.subHeaders.subHeader[i].html.text); | |
} | |
} else { | |
subheaders.push((dct.subHeaders.subHeader.html) ? dct.subHeaders.subHeader.html.text : ''); | |
} | |
return subheaders; | |
}; | |
return methods; | |
}, | |
/** | |
* The body can contain many types of things. If it's a photo, we | |
* delegate to photo method. If it's a list, we delegate to the | |
* embedded list method. Otherwise, provides html text. | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
body : function(dct, methods) { | |
var body = ''; | |
if (dct.body.lead || dct.body.introduction) { | |
body += '<div class="lead-introduction">'; | |
} | |
body += dct.body.lead ? '<div class="lead">' + dct.body.lead + '</div> ' : ''; | |
body += dct.body.introduction ? dct.body.introduction + ' ' : ''; | |
if (dct.body.lead || dct.body.introduction) { | |
body += '</div>'; | |
} | |
body += dct.body.text ? '<div class="text">' + dct.body.text + '</div>' : ''; | |
methods.body = function() { return body; }; | |
methods.bodyText = function() { return dct.body.text; }; | |
methods.bodyLead = function() { return dct.body.lead; }; | |
methods.bodyIntroduction = function() { return dct.body.introduction; }; | |
if (dct.body.photo) { | |
methods = this.photo(dct.body, methods); | |
} | |
if (dct.body.embeddedList) { | |
methods = this.embeddedList(dct.body, methods); | |
} | |
if(dct.body.altMedia) { | |
methods = this.altMedia(dct.body, methods); | |
} | |
return methods; | |
}, | |
/** | |
* Provides text for footer information | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
footer : function(dct, methods) { | |
methods.footerText = function() { return dct.footer.text || ''; }; | |
methods.footerLegalCopy = function() { return dct.footer.legalCopy || ''; }; | |
return methods; | |
}, | |
/** | |
* Photos need to be available directly. We create as many methods | |
* as we have image replicants, and delegate for annotations | |
* if they exist. | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
photo : function(dct, methods) { | |
methods.photo = {}; | |
var i, | |
il, | |
images = dct.photo.images.image, | |
thisImage, | |
thisSource, | |
that = this, | |
templateToUse = CN.schemaParser.templates && CN.schemaParser.templates.annotations ? | |
CN.schemaParser.templates.annotations : | |
CN.schemaParser.defaultTemplates.annotations; | |
if (Object.prototype.toString.call(images) === '[object Array]') { | |
for (i = 0, il = images.length; i < il; i++) { | |
(function() { // scope for i | |
var image = images[i]; | |
methods.photo[image.type] = function() { | |
return image.source; | |
}; | |
methods.photo[image.type].annotations = function() { | |
return templateToUse(image); | |
}; | |
})(); | |
} | |
} else { | |
methods.photo[images.type] = function() { | |
return images.source; | |
}; | |
methods.photo[images.type].annotations = function() { | |
return templateToUse(images); | |
}; | |
} | |
methods.photo.alt = function() { return dct.photo.altText; }; | |
methods.photo.caption = function() { return dct.photo.caption; }; | |
methods.photo.credit = function() { return dct.photo.credit; }; | |
methods.photo.url = function() { return dct.photo.URL; }; | |
return methods; | |
}, | |
/** | |
* This creates a complete object for all mediaItem replicants in a given | |
* DCT. For now, going to do one big method and possibly refactor later. | |
* Differs from photo in that the method is mediaItems() rather than adding | |
* methods for caption(), credit() etc. Those become simply properties on | |
* the return object from mediaItems(). | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object containing mediaItems | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
mediaItems : function(dct, methods) { | |
var that = this, | |
templateToUse = CN.schemaParser.templates && CN.schemaParser.templates.annotations ? | |
CN.schemaParser.templates.annotations : | |
CN.schemaParser.defaultTemplates.annotations; | |
// TODO: memoize some shizz | |
/* | |
Returns an array of mediaItem | |
@param placement {String|null} The placement property to match | |
@param type {String|null} The mediaItem type to match | |
@param sequence {Number|String} Which mediaItem to use, or "all" | |
*/ | |
methods.mediaItems = function(placement, type, sequence) { | |
placement = placement || null; | |
type = type || null; | |
sequence = sequence || 0; | |
var results = [], | |
result = null, | |
mediaItems = dct.mediaItems.mediaItem, | |
i, il, | |
j, jl, | |
props, | |
formats; | |
if (Object.prototype.toString.call(mediaItems) === '[object Array]') { | |
for (i = 0, il = mediaItems.length; i < il; i++) { | |
results[i] = mediaItems[i]; | |
} | |
} else { | |
results[0] = mediaItems; | |
} | |
// Filters results by placement | |
if (placement !== null) { | |
results = results.filter(function(res) { | |
props = res.properties && res.properties.property ? | |
CN.utils.mapPropertyArray(res.properties.property) : | |
false; | |
return props.placement && props.placement === placement ? | |
true : | |
false; | |
}); | |
} | |
// Filters results by type | |
if (type !== null) { | |
results = results.filter(function(res) { | |
return res.type === type ? true : false; | |
}); | |
} | |
// If we want "all", set result = results, else filter to | |
// one result matching the sequence provided | |
// Either way, result is an array of 1+ or null | |
result = (sequence === 'all') ? results : ([results[sequence]] || null); | |
if (result) { | |
for (i = 0, il = result.length; i < il; i++) { | |
// Handle formats | |
if (result[i].formats && result[i].formats.format) { | |
formats = result[i].formats.format; | |
if (Object.prototype.toString.call(formats) === '[object Array]') { | |
for (j = 0, jl = formats.length; j < jl; j++) { | |
(function() { // scope for i | |
var format = formats[j]; | |
result[i][format.name] = function() { | |
return format.source; | |
}; | |
result[i][format.name].annotations = function() { | |
return templateToUse(format); | |
}; | |
})(); | |
} | |
} else { | |
result[i][formats.name] = function() { | |
return formats.source; | |
}; | |
result[i][formats.name].annotations = function() { | |
return templateToUse(formats); | |
}; | |
} | |
} | |
result[i].getProperties = function() { | |
return result.properties && result.properties.property ? | |
CN.utils.mapPropertyArray(result.properties.property) : | |
false; | |
}; | |
} | |
} | |
return result; | |
}; | |
return methods; | |
}, | |
/** | |
* Provides html text for rubric information | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json unitMetaData object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
rubric : function(dct, methods) { | |
if (dct.rubric) { | |
var ret = '', | |
alt = '', | |
img = ''; | |
if (dct.rubric.graphic) { | |
alt = dct.rubric.graphic.altText || ''; | |
img = '<img src="' + dct.rubric.graphic.image + '" alt="' + alt + '" />'; | |
if (dct.rubric.graphic.URL) { | |
ret = '<a href="' + dct.rubric.graphic.URL + '">' + img + '</a>'; | |
} else { | |
ret = img; | |
} | |
} else { | |
if (dct.rubric.html.URL) { | |
ret += '<a href="' + dct.rubric.html.URL + '">' + dct.rubric.html.text + '</a>'; | |
} else { | |
ret += dct.rubric.html.text; | |
} | |
} | |
methods.rubric = function() { return ret; }; | |
} | |
return methods; | |
}, | |
/** | |
* Provides an object for contributor information, rather than | |
* html text directly. This allows the template for contributors | |
* to be overridden in the implementation code. | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
byline : function(dct, methods) { | |
if (dct.byline && dct.byline.contributors) { | |
var ret = CN.schemaParser.templates && CN.schemaParser.templates.contributors ? | |
CN.schemaParser.templates.contributors(dct.byline.contributors.contributor) : | |
CN.schemaParser.defaultTemplates.contributors(dct.byline.contributors.contributor); | |
methods.contributors = function() { return ret; }; | |
} | |
return methods; | |
}, | |
/** | |
* Provides a date string for html insertion. | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
displayDate : function(dct, methods) { | |
if (dct.displayDate.date) { | |
methods.displayDate = function() { | |
var date = new Date(CN.date.isoToDate(CN.date.convertIwDateToIso(dct.displayDate.date))); | |
return CN.date.format(date, dct.displayDate.format || 'MMMM yyyy'); | |
}; | |
} | |
return methods; | |
}, | |
/** | |
* Provides text for photo credit information. | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
photoCredits : function(dct, methods) { | |
if (dct.photoCredits) { | |
methods.photoCredits = function() { return dct.photoCredits; }; | |
} | |
return methods; | |
}, | |
/** | |
* Provides text for embedded list information. | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
embeddedList : function(dct, methods) { | |
if (dct.embeddedList) { | |
var list = '<ul class="embedded-list">', | |
listItem, | |
i, | |
il; | |
if (Object.prototype.toString.call(dct.embeddedList.listItem) === '[object Array]') { | |
for (i = 0, il = dct.embeddedList.listItem.length; i < il; i++) { | |
(function() { // scope for i | |
var newItem = '<li>' + dct.embeddedList.listItem[i] + '</li>'; | |
list += newItem; | |
})(); | |
} | |
} else { | |
list += '<li>' + dct.embeddedList.listItem + '</li>'; | |
} | |
list += '</ul>'; | |
methods.embeddedList = function() { return list; }; | |
} | |
return methods; | |
}, | |
/** | |
* Provides video id for video slides in slideshow. | |
* @memberOf CN.schemaParser.schemas | |
* @param {Object} dct A json object | |
* @param {Object} methods The methods for this schema parser | |
* @return {Object} The modified methods for this schema parser | |
*/ | |
altMedia : function(dct,methods){ | |
methods.altMedia = {}; | |
methods.altMedia['type'] = dct.altMedia.type; | |
var itemProps = {}; | |
if(dct.altMedia.properties && dct.altMedia.properties.property){ | |
itemProps = CN.utils.mapPropertyArray(dct.altMedia.properties.property); | |
if(itemProps.id){ | |
methods.altMedia['videoID']= function(){ | |
return itemProps.id; | |
}; | |
} | |
if(itemProps.playerID) { | |
methods.altMedia['playerID'] = function() { | |
return itemProps.playerID; | |
} | |
} | |
} | |
return methods; | |
} | |
}; | |
/** | |
* Default HTML templates for content that can have custom HTML per site, | |
* but that we want to provide a baseline template for. | |
* | |
* @property defaultTemplates | |
* @static | |
*/ | |
CN.schemaParser.defaultTemplates = { | |
/** | |
* Pass in a photo, get annotations. This is coupled to the | |
* jquery.annotations.js plugin. We assume that the top/left data is | |
* provided correctly by the DCT. Because it can't be accessed directly, | |
* it's set as a template object so both photo and mediaItems can call it | |
* and get HTML. | |
* | |
* @memberOf CN.schemaParser.defaultTemplates | |
* @requires jquery.annotations.js | |
* @param {Object} dct A json photo (or format) object | |
* @return {String} Annotations HTML for DOM insertion | |
*/ | |
annotations : function(dct) { | |
var i, | |
il, | |
html = '', | |
props; | |
if (dct.annotations) { | |
html += '<ul class="annotations">'; | |
if (Object.prototype.toString.call(dct.annotations.annotation) === '[object Array]') { | |
// if array | |
for (i = 0, il = dct.annotations.annotation.length; i < il; i++) { | |
props = CN.utils.mapPropertyArray(dct.annotations.annotation[i].properties.property); | |
html += '<li class="annotation" data-left="'; | |
html += props.left; | |
html += '" data-top="'; | |
html += props.top; | |
html += '">'; | |
html += '<a href="#" class="annotation-trigger">Note:</a>'; | |
html += '<div class="annotation-content">'; | |
html += dct.annotations.annotation[i].body.text; | |
html += '</div>'; | |
html += '</li>'; | |
} | |
} else { | |
// if object | |
props = CN.utils.mapPropertyArray(dct.annotations.annotation.properties.property); | |
html += '<li class="annotation" data-left="'; | |
html += props.left; | |
html += '" data-top="'; | |
html += props.top; | |
html += '">'; | |
html += '<a href="#" class="annotation-trigger">Note:</a>'; | |
html += '<div class="annotation-content">'; | |
html += dct.annotations.annotation.body.text; | |
html += '</div>'; | |
html += '</li>'; | |
} | |
html += '</ul>'; | |
} | |
return html; | |
}, | |
/** | |
* Returns HTML for contributors as a default template, | |
* can be overridden in the implementation code. | |
* | |
* @param {Object} dct A json object of contributors | |
* @return {String} The contributors HTML for DOM insertion | |
*/ | |
contributors : function(dct) { | |
var i, | |
il, | |
html = ''; | |
html += '<div class="contributors">'; | |
if (Object.prototype.toString.call(dct) === '[object Array]') { | |
for (i = 0, il = dct.length; i < il; i++) { | |
(function() { // scope for i | |
var contributor = dct[i]; | |
html += '<p><span class="contributor">'; | |
html += contributor.label ? '<span class="label">' + contributor.label + '</span> ' : ''; | |
html += contributor.name ? '<span class="name">' + contributor.name + '</span>' : ''; | |
html += '</span></p>'; | |
})(); | |
} | |
} else { | |
html += '<p><span class="contributor">'; | |
html += dct.label ? '<span class="label">' + dct.label + '</span> ' : ''; | |
html += dct.name ? '<span class="name">' + dct.name + '</span>' : ''; | |
html += '</span></p>'; | |
} | |
html += '</div>'; | |
return html; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment