/** * @license * Copyright (c) 2015 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ 'use strict'; // jshint node:true var jsdoc = require('./jsdoc'); var dom5 = require('dom5'); /** Properties on element prototypes that are purely configuration. */ var ELEMENT_CONFIGURATION = [ 'attached', 'attributeChanged', 'configure', 'constructor', 'created', 'detached', 'enableCustomStyleProperties', 'extends', 'hostAttributes', 'is', 'listeners', 'mixins', 'properties', 'ready', 'registered' ]; /** Tags understood by the annotation process, to be removed during `clean`. */ var HANDLED_TAGS = [ 'param', 'return', 'type', ]; /** * Annotates Hydrolysis descriptors, processing any `desc` properties as JSDoc. * * You probably want to use a more specialized version of this, such as * `annotateElement`. * * Processed JSDoc values will be made available via the `jsdoc` property on a * descriptor node. * * @param {Object} descriptor The descriptor node to process. * @return {Object} The descriptor that was given. */ function annotate(descriptor) { if (!descriptor || descriptor.jsdoc) return descriptor; if (typeof descriptor.desc === 'string') { descriptor.jsdoc = jsdoc.parseJsdoc(descriptor.desc); // We want to present the normalized form of a descriptor. descriptor.jsdoc.orig = descriptor.desc; descriptor.desc = descriptor.jsdoc.description; } return descriptor; } /** * Annotates @event, @hero, & @demo tags */ function annotateElementHeader(descriptor) { if (descriptor.events) { descriptor.events.forEach(function(event) { _annotateEvent(event); }); descriptor.events.sort( function(a,b) { return a.name.localeCompare(b.name); }); } descriptor.demos = []; if (descriptor.jsdoc && descriptor.jsdoc.tags) { descriptor.jsdoc.tags.forEach( function(tag) { switch(tag.tag) { case 'hero': descriptor.hero = tag.name || 'hero.png'; break; case 'demo': descriptor.demos.push({ desc: tag.description || 'demo', path: tag.name || 'demo/index.html' }); } }); } } function matchByName(propa, propb) { return propa.name == propb.name; } function copyProperties(from, to, behaviorsByName) { if (from.properties) { from.properties.forEach(function(fromProp){ for (var toProp, i = 0; i < to.properties.length; i++) { toProp = to.properties[i]; if (fromProp.name === toProp.name) { return; } } var newProp = {__fromBehavior: from.is}; if (fromProp.__fromBehavior) { return; } Object.keys(fromProp).forEach(function(propertyField){ newProp[propertyField] = fromProp[propertyField]; }); to.properties.push(newProp); }); from.events.forEach(function(fromEvent){ for (var toEvent, i = 0; i < to.events.length; i++) { toEvent = to.events[i]; if (fromEvent.name === toEvent.name) { return; } } if (fromEvent.__fromBehavior) { return; } var newEvent = Object.create(fromEvent); newEvent.__fromBehavior = from.is; to.events.push(newEvent); }); } if (!from.behaviors) { return; } for (var i = from.behaviors.length - 1; i >= 0; i--) { var behavior = from.behaviors[i]; var definedBehavior = behaviorsByName[behavior] || behaviorsByName[behavior.symbol]; if (!definedBehavior) { console.warn("Behavior " + behavior + " not found when mixing " + "properties into " + to.is + "!"); return; } copyProperties(definedBehavior, to, behaviorsByName); } } function mixinBehaviors(descriptor, behaviorsByName) { if (descriptor.behaviors) { for (var i = descriptor.behaviors.length - 1; i >= 0; i--) { var behavior = descriptor.behaviors[i]; if (!behaviorsByName[behavior]) { console.warn("Behavior " + behavior + " not found when mixing " + "properties into " + descriptor.is + "!"); break; } var definedBehavior = behaviorsByName[behavior]; copyProperties(definedBehavior, descriptor, behaviorsByName); } } } /** * Annotates documentation found within a Hydrolysis element descriptor. Also * supports behaviors. * * If the element was processed via `hydrolize`, the element's documentation * will also be extracted via its . * * @param {Object} descriptor The element descriptor. * @return {Object} The descriptor that was given. */ function annotateElement(descriptor, behaviorsByName) { if (!descriptor.desc && descriptor.type === 'element') { descriptor.desc = _findElementDocs(descriptor.is, descriptor.domModule, descriptor.scriptElement); } annotate(descriptor); // The `` is too low level for most needs, and it is _not_ // serializable. So we drop it now that we've extracted all the useful bits // from it. // TODO: Don't worry about serializability here, provide an API to get JSON. delete descriptor.domModule; mixinBehaviors(descriptor, behaviorsByName); // Descriptors that should have their `desc` properties parsed as JSDoc. descriptor.properties.forEach(function(property) { // Feature properties are special, configuration is really just a matter of // inheritance... annotateProperty(property, descriptor.abstract); }); // It may seem like overkill to always sort, but we have an assumption that // these properties are typically being consumed by user-visible tooling. // As such, it's good to have consistent output/ordering to aid the user. descriptor.properties.sort(function(a, b) { // Private properties are always last. if (a.private && !b.private) { return 1; } else if (!a.private && b.private) { return -1; // Otherwise, we're just sorting alphabetically. } else { return a.name.localeCompare(b.name); } }); annotateElementHeader(descriptor); return descriptor; } /** * Annotates behavior descriptor. * @param {Object} descriptor behavior descriptor * @return {Object} descriptor passed in as param */ function annotateBehavior(descriptor, behaviorsByName) { annotate(descriptor); annotateElementHeader(descriptor); return descriptor; } /** * Annotates event documentation */ function _annotateEvent(descriptor) { annotate(descriptor); // process @event var eventTag = jsdoc.getTag(descriptor.jsdoc, 'event'); descriptor.name = eventTag ? eventTag.description : "N/A"; // process @params descriptor.params = (descriptor.jsdoc.tags || []) .filter( function(tag) { return tag.tag === 'param'; }) .map( function(tag) { return { type: tag.type || "N/A", desc: tag.description, name: tag.name || "N/A" }; }); // process @params return descriptor; } /** * Annotates documentation found about a Hydrolysis property descriptor. * * @param {Object} descriptor The property descriptor. * @param {boolean} ignoreConfiguration If true, `configuration` is not set. * @return {Object} The descriptior that was given. */ function annotateProperty(descriptor, ignoreConfiguration) { annotate(descriptor); if (descriptor.name[0] === '_' || jsdoc.hasTag(descriptor.jsdoc, 'private')) { descriptor.private = true; } if (!ignoreConfiguration && ELEMENT_CONFIGURATION.indexOf(descriptor.name) !== -1) { descriptor.private = true; descriptor.configuration = true; } // @type JSDoc wins descriptor.type = jsdoc.getTag(descriptor.jsdoc, 'type', 'type') || descriptor.type; if (descriptor.type.match(/^function/i)) { _annotateFunctionProperty(descriptor); } // @default JSDoc wins var defaultTag = jsdoc.getTag(descriptor.jsdoc, 'default'); if (defaultTag !== null) { var newDefault = (defaultTag.name || '') + (defaultTag.description || ''); if (newDefault !== '') { descriptor.default = newDefault; } } return descriptor; } /** @param {Object} descriptor */ function _annotateFunctionProperty(descriptor) { descriptor.function = true; var returnTag = jsdoc.getTag(descriptor.jsdoc, 'return'); if (returnTag) { descriptor.return = { type: returnTag.type, desc: returnTag.description, }; } var paramsByName = {}; (descriptor.params || []).forEach(function(param) { paramsByName[param.name] = param; }); (descriptor.jsdoc && descriptor.jsdoc.tags || []).forEach(function(tag) { if (tag.tag !== 'param') return; var param = paramsByName[tag.name]; if (!param) { return; } param.type = tag.type || param.type; param.desc = tag.description; }); } /** * Converts raw features into an abstract `Polymer.Base` element. * * Note that docs on this element _are not processed_. You must call * `annotateElement` on it yourself if you wish that. * * @param {Array} features * @return {ElementDescriptor} */ function featureElement(features) { var properties = features.reduce(function(result, feature) { return result.concat(feature.properties); }, []); return { type: 'element', is: 'Polymer.Base', abstract: true, properties: properties, desc: '`Polymer.Base` acts as a base prototype for all Polymer ' + 'elements. It is composed via various calls to ' + '`Polymer.Base._addFeature()`.\n' + '\n' + 'The properties reflected here are the combined view of all ' + 'features found in this library. There may be more properties ' + 'added via other libraries, as well.', }; } /** * Cleans redundant properties from a descriptor, assuming that you have already * called `annotate`. * * @param {Object} descriptor */ function clean(descriptor) { if (!descriptor.jsdoc) return; // The doctext was written to `descriptor.desc` delete descriptor.jsdoc.description; delete descriptor.jsdoc.orig; var cleanTags = []; (descriptor.jsdoc.tags || []).forEach(function(tag) { // Drop any tags we've consumed. if (HANDLED_TAGS.indexOf(tag.tag) !== -1) return; cleanTags.push(tag); }); if (cleanTags.length === 0) { // No tags? no docs left! delete descriptor.jsdoc; } else { descriptor.jsdoc.tags = cleanTags; } } /** * Cleans redundant properties from an element, assuming that you have already * called `annotateElement`. * * @param {ElementDescriptor|BehaviorDescriptor} element */ function cleanElement(element) { clean(element); element.properties.forEach(cleanProperty); } /** * Cleans redundant properties from a property, assuming that you have already * called `annotateProperty`. * * @param {PropertyDescriptor} property */ function cleanProperty(property) { clean(property); } /** * Parse elements defined only in comments. * @param {comments} Array A list of comments to parse. * @return {ElementDescriptor} A list of pseudo-elements. */ function parsePseudoElements(comments) { var elements = []; comments.forEach(function(comment) { var parsed = jsdoc.parseJsdoc(comment); var pseudoTag = jsdoc.getTag(parsed, 'pseudoElement', 'name'); if (pseudoTag) { parsed.is = pseudoTag; parsed.jsdoc = {description: parsed.description, tags: parsed.tags}; parsed.properties = []; parsed.desc = parsed.description; parsed.description = undefined; parsed.tags = undefined; annotateElementHeader(parsed); elements.push(parsed); } }); return elements; } /** * @param {string} elementId * @param {DocumentAST} domModule * @param {DocumentAST} scriptElement The script that the element was defined in. */ function _findElementDocs(elementId, domModule, scriptElement) { // Note that we concatenate docs from all sources if we find them. // element can be defined in: // html comment right before dom-module // html commnet right before script defining the module, if dom-module is empty var found = []; // Do we have a HTML comment on the `` or `