UNPKG

6.17 kBJavaScriptView Raw
1/*global env: true */
2/**
3 @overview
4 @author Michael Mathews <micmath@gmail.com>
5 @license Apache License 2.0 - See file 'LICENSE.md' in this project.
6 */
7
8/**
9 Functionality related to JSDoc tags.
10 @module jsdoc/tag
11 @requires jsdoc/tag/dictionary
12 @requires jsdoc/tag/validator
13 @requires jsdoc/tag/type
14 */
15'use strict';
16
17var jsdoc = {
18 tag: {
19 dictionary: require('jsdoc/tag/dictionary'),
20 validator: require('jsdoc/tag/validator'),
21 type: require('jsdoc/tag/type')
22 },
23 util: {
24 logger: require('jsdoc/util/logger')
25 }
26};
27var path = require('jsdoc/path');
28var util = require('util');
29
30// Check whether the text is the same as a symbol name with leading or trailing whitespace. If so,
31// the whitespace must be preserved, and the text cannot be trimmed.
32function mustPreserveWhitespace(text, meta) {
33 return meta && meta.code && meta.code.name === text && text.match(/(?:^\s+)|(?:\s+$)/);
34}
35
36function trim(text, opts, meta) {
37 var indentMatcher;
38 var match;
39
40 opts = opts || {};
41 text = String(typeof text !== 'undefined' ? text : '');
42
43 if ( mustPreserveWhitespace(text, meta) ) {
44 text = util.format('"%s"', text);
45 }
46 else if (opts.keepsWhitespace) {
47 text = text.replace(/^[\n\r\f]+|[\n\r\f]+$/g, '');
48 if (opts.removesIndent) {
49 match = text.match(/^([ \t]+)/);
50 if (match && match[1]) {
51 indentMatcher = new RegExp('^' + match[1], 'gm');
52 text = text.replace(indentMatcher, '');
53 }
54 }
55 }
56 else {
57 text = text.replace(/^\s+|\s+$/g, '');
58 }
59
60 return text;
61}
62
63function addHiddenProperty(obj, propName, propValue) {
64 Object.defineProperty(obj, propName, {
65 value: propValue,
66 writable: true,
67 enumerable: !!global.env.opts.debug,
68 configurable: true
69 });
70}
71
72function parseType(tag, tagDef, meta) {
73 try {
74 return jsdoc.tag.type.parse(tag.text, tagDef.canHaveName, tagDef.canHaveType);
75 }
76 catch (e) {
77 jsdoc.util.logger.error(
78 'Unable to parse a tag\'s type expression%s with tag title "%s" and text "%s": %s',
79 meta.filename ? ( ' for source file ' + path.join(meta.path, meta.filename) ) : '',
80 tag.originalTitle,
81 tag.text,
82 e.message
83 );
84 return {};
85 }
86}
87
88function processTagText(tag, tagDef, meta) {
89 var tagType;
90
91 if (tagDef.onTagText) {
92 tag.text = tagDef.onTagText(tag.text);
93 }
94
95 if (tagDef.canHaveType || tagDef.canHaveName) {
96 /** The value property represents the result of parsing the tag text. */
97 tag.value = {};
98
99 tagType = parseType(tag, tagDef, meta);
100
101 // It is possible for a tag to *not* have a type but still have
102 // optional or defaultvalue, e.g. '@param [foo]'.
103 // Although tagType.type.length == 0 we should still copy the other properties.
104 if (tagType.type) {
105 if (tagType.type.length) {
106 tag.value.type = {
107 names: tagType.type
108 };
109 addHiddenProperty(tag.value.type, 'parsedType', tagType.parsedType);
110 }
111
112 ['optional', 'nullable', 'variable', 'defaultvalue'].forEach(function(prop) {
113 if (typeof tagType[prop] !== 'undefined') {
114 tag.value[prop] = tagType[prop];
115 }
116 });
117 }
118
119 if (tagType.text && tagType.text.length) {
120 tag.value.description = tagType.text;
121 }
122
123 if (tagDef.canHaveName) {
124 // note the dash is a special case: as a param name it means "no name"
125 if (tagType.name && tagType.name !== '-') { tag.value.name = tagType.name; }
126 }
127 }
128 else {
129 tag.value = tag.text;
130 }
131}
132
133/**
134 * Replace the existing tag dictionary with a new tag dictionary.
135 *
136 * Used for testing only. Do not call this method directly. Instead, call
137 * {@link module:jsdoc/doclet._replaceDictionary}, which also updates this module's tag dictionary.
138 *
139 * @private
140 * @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
141 */
142exports._replaceDictionary = function _replaceDictionary(dict) {
143 jsdoc.tag.dictionary = dict;
144};
145
146/**
147 Constructs a new tag object. Calls the tag validator.
148 @class
149 @classdesc Represents a single doclet tag.
150 @param {string} tagTitle
151 @param {string=} tagBody
152 @param {object=} meta
153 */
154var Tag = exports.Tag = function(tagTitle, tagBody, meta) {
155 var tagDef;
156 var trimOpts;
157
158 meta = meta || {};
159
160 this.originalTitle = trim(tagTitle);
161
162 /** The title of the tag (for example, `title` in `@title text`). */
163 this.title = jsdoc.tag.dictionary.normalise(this.originalTitle);
164
165 tagDef = jsdoc.tag.dictionary.lookUp(this.title);
166 trimOpts = {
167 keepsWhitespace: tagDef.keepsWhitespace,
168 removesIndent: tagDef.removesIndent
169 };
170
171 /**
172 * The text following the tag (for example, `text` in `@title text`).
173 *
174 * Whitespace is trimmed from the tag text as follows:
175 *
176 * + If the tag's `keepsWhitespace` option is falsy, all leading and trailing whitespace are
177 * removed.
178 * + If the tag's `keepsWhitespace` option is set to `true`, leading and trailing whitespace are
179 * not trimmed, unless the `removesIndent` option is also enabled.
180 * + If the tag's `removesIndent` option is set to `true`, any indentation that is shared by
181 * every line in the string is removed. This option is ignored unless `keepsWhitespace` is set
182 * to `true`.
183 *
184 * **Note**: If the tag text is the name of a symbol, and the symbol's name includes leading or
185 * trailing whitespace (for example, the property names in `{ ' ': true, ' foo ': false }`),
186 * the tag text is not trimmed. Instead, the tag text is wrapped in double quotes to prevent the
187 * whitespace from being trimmed.
188 */
189 this.text = trim(tagBody, trimOpts, meta);
190
191 if (this.text) {
192 processTagText(this, tagDef, meta);
193 }
194
195 jsdoc.tag.validator.validate(this, tagDef, meta);
196};