UNPKG

7.25 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt
7 * The complete set of authors may be found at
8 * http://polymer.github.io/AUTHORS.txt
9 * The complete set of contributors may be found at
10 * http://polymer.github.io/CONTRIBUTORS.txt
11 * Code distributed by Google as part of the polymer project is also
12 * subject to an additional IP rights grant found at
13 * http://polymer.github.io/PATENTS.txt
14 */
15Object.defineProperty(exports, "__esModule", { value: true });
16const doctrine = require("doctrine");
17const model_1 = require("../model/model");
18/**
19 * Given a JSDoc string (minus opening/closing comment delimiters), extract its
20 * description and tags.
21 */
22function parseJsdoc(docs) {
23 docs = removeLeadingAsterisks(docs);
24 const d = doctrine.parse(docs, {
25 unwrap: false,
26 // lineNumbers: true,
27 preserveWhitespace: true,
28 });
29 // Strip any leading and trailing newline characters in the
30 // description of multiline comments for readibility.
31 // TODO(rictic): figure out if we can trim() here or not. Something something
32 // markdown?
33 const description = d.description && d.description.replace(/^\n+|\n+$/g, '');
34 return { description, tags: parseCustomTags(d.tags) };
35}
36exports.parseJsdoc = parseJsdoc;
37// Tags with a name: @title name description
38const tagsWithNames = new Set([
39 'appliesMixin',
40 'demo',
41 'hero',
42 'mixinFunction',
43 'polymerBehavior',
44 'pseudoElement'
45]);
46const firstWordAndRest = /^\s*(\S*)\s*(.*)$/;
47function parseCustomTags(tags) {
48 return tags.map((tag) => {
49 if (tag.description != null && tagsWithNames.has(tag.title)) {
50 const match = firstWordAndRest.exec(tag.description);
51 if (match != null) {
52 const name = match[1];
53 const description = match[2];
54 return Object.assign({}, tag, { name,
55 description });
56 }
57 }
58 return tag;
59 });
60}
61/**
62 * removes leading *, and any space before it
63 */
64function removeLeadingAsterisks(description) {
65 return description.split('\n')
66 .map(function (line) {
67 // remove leading '\s*' from each line
68 const match = line.trim().match(/^[\s]*\*\s?(.*)$/);
69 return match ? match[1] : line;
70 })
71 .join('\n');
72}
73exports.removeLeadingAsterisks = removeLeadingAsterisks;
74function hasTag(jsdoc, title) {
75 return getTag(jsdoc, title) !== undefined;
76}
77exports.hasTag = hasTag;
78/**
79 * Finds the first JSDoc tag matching `title`.
80 */
81function getTag(jsdoc, title) {
82 return jsdoc && jsdoc.tags && jsdoc.tags.find((t) => t.title === title);
83}
84exports.getTag = getTag;
85function unindent(text) {
86 if (!text) {
87 return text;
88 }
89 const lines = text.replace(/\t/g, ' ').split('\n');
90 const indent = lines.reduce(function (prev, line) {
91 if (/^\s*$/.test(line)) {
92 return prev; // Completely ignore blank lines.
93 }
94 const lineIndent = line.match(/^(\s*)/)[0].length;
95 if (prev === null) {
96 return lineIndent;
97 }
98 return lineIndent < prev ? lineIndent : prev;
99 }, 0);
100 return lines
101 .map(function (l) {
102 return l.substr(indent);
103 })
104 .join('\n');
105}
106exports.unindent = unindent;
107function isAnnotationEmpty(docs) {
108 return docs === undefined ||
109 docs.tags.length === 0 && docs.description.trim() === '';
110}
111exports.isAnnotationEmpty = isAnnotationEmpty;
112const privacyTags = new Set(['public', 'private', 'protected']);
113function getPrivacy(jsdoc) {
114 return jsdoc && jsdoc.tags &&
115 jsdoc.tags.filter((t) => privacyTags.has(t.title))
116 .map((t) => t.title)[0];
117}
118exports.getPrivacy = getPrivacy;
119/**
120 * Returns the mixin applications, in the form of ScannedReferences, for the
121 * jsdocs of class.
122 *
123 * The references are returned in presumed order of application - from furthest
124 * up the prototype chain to closest to the subclass.
125 */
126function getMixinApplications(document, node, docs, warnings, path) {
127 // TODO(justinfagnani): remove @mixes support
128 const appliesMixinAnnotations = docs.tags.filter((tag) => tag.title === 'appliesMixin' || tag.title === 'mixes');
129 return appliesMixinAnnotations
130 .map((annotation) => {
131 const mixinId = annotation.name;
132 // TODO(justinfagnani): we need source ranges for jsdoc
133 // annotations
134 const sourceRange = document.sourceRangeForNode(node);
135 if (mixinId === undefined) {
136 warnings.push(new model_1.Warning({
137 code: 'class-mixes-annotation-no-id',
138 message: '@appliesMixin annotation with no identifier. Usage `@appliesMixin MixinName`',
139 severity: model_1.Severity.WARNING,
140 sourceRange,
141 parsedDocument: document
142 }));
143 return;
144 }
145 return new model_1.ScannedReference('element-mixin', mixinId, sourceRange, undefined, path);
146 })
147 .filter((m) => m !== undefined);
148}
149exports.getMixinApplications = getMixinApplications;
150function extractDemos(jsdoc) {
151 if (!jsdoc || !jsdoc.tags) {
152 return [];
153 }
154 const demos = [];
155 const demoUrls = new Set();
156 for (const tag of jsdoc.tags.filter((tag) => tag.title === 'demo' && tag.name)) {
157 const demoUrl = tag.name;
158 if (demoUrls.has(demoUrl)) {
159 continue;
160 }
161 demoUrls.add(demoUrl);
162 demos.push({
163 desc: tag.description || undefined,
164 path: demoUrl,
165 });
166 }
167 return demos;
168}
169exports.extractDemos = extractDemos;
170function join(...jsdocs) {
171 return {
172 description: jsdocs.map((jsdoc) => jsdoc && jsdoc.description || '')
173 .join('\n\n')
174 .trim(),
175 tags: jsdocs.map((jsdoc) => jsdoc && jsdoc.tags || [])
176 .reduce((acc, tags) => acc.concat(tags)),
177 };
178}
179exports.join = join;
180/**
181 * Assume that if the same symbol is documented in multiple places, the longer
182 * description is probably the intended one.
183 *
184 * TODO(rictic): unify logic with join(...)'s above.
185 */
186function pickBestDescription(...descriptions) {
187 let description = '';
188 for (const desc of descriptions) {
189 if (desc && desc.length > description.length) {
190 description = desc;
191 }
192 }
193 return description;
194}
195exports.pickBestDescription = pickBestDescription;
196/**
197 * Extracts the description from a jsdoc annotation and uses
198 * known descriptive tags if no explicit description is set.
199 */
200function getDescription(jsdocAnn) {
201 if (jsdocAnn.description) {
202 return jsdocAnn.description;
203 }
204 // These tags can be used to describe a field.
205 // e.g.:
206 // /** @type {string} the name of the animal */
207 // this.name = name || 'Rex';
208 const tagSet = new Set(['public', 'private', 'protected', 'type']);
209 for (const tag of jsdocAnn.tags) {
210 if (tagSet.has(tag.title) && tag.description) {
211 return tag.description;
212 }
213 }
214}
215exports.getDescription = getDescription;
216//# sourceMappingURL=jsdoc.js.map
\No newline at end of file