diff --git a/lib/jsdoc/tag/dictionary/definitions.js b/lib/jsdoc/tag/dictionary/definitions.js index 0bd66713..c0f370aa 100644 --- a/lib/jsdoc/tag/dictionary/definitions.js +++ b/lib/jsdoc/tag/dictionary/definitions.js @@ -539,12 +539,22 @@ exports.defineTags = function(dictionary) { dictionary.defineTag('requires', { mustHaveValue: true, onTagged: function(doclet, tag) { - var modName = firstWordOf(tag.value); - if (modName.indexOf('module:') !== 0) { - modName = 'module:'+modName; + var requiresName; + + // inline link tags are passed through as-is so that `@requires {@link foo}` works + if ( require('jsdoc/tag/inline').isInlineTag(tag.value, 'link\\S*') ) { + requiresName = tag.value; } + // otherwise, assume it's a module + else { + requiresName = firstWordOf(tag.value); + if (requiresName.indexOf('module:') !== 0) { + requiresName = 'module:' + requiresName; + } + } + if (!doclet.requires) { doclet.requires = []; } - doclet.requires.push(modName); + doclet.requires.push(requiresName); } }); diff --git a/lib/jsdoc/tag/inline.js b/lib/jsdoc/tag/inline.js index 8be46e26..952b5af9 100644 --- a/lib/jsdoc/tag/inline.js +++ b/lib/jsdoc/tag/inline.js @@ -35,6 +35,45 @@ * @return {string} An updated version of the complete string. */ +/** + * Create a regexp that matches a specific inline tag, or all inline tags. + * + * @private + * @memberof module:jsdoc/tag/inline + * @param {?string} tagName - The inline tag that the regexp will match. May contain regexp + * characters. If omitted, matches any string. + * @param {?string} prefix - A prefix for the regexp. Defaults to an empty string. + * @param {?string} suffix - A suffix for the regexp. Defaults to an empty string. + * @returns {RegExp} A regular expression that matches the requested inline tag. + */ +function regExpFactory(tagName, prefix, suffix) { + var tagMatcher = tagName || '\\S+'; + + prefix = prefix || ''; + suffix = suffix || ''; + + return new RegExp(prefix + '\\{@' + tagMatcher + '\\s+((?:.|\n)+?)\\}' + suffix, 'gi'); +} + +/** + * Check whether a string is an inline tag. You can check for a specific inline tag or for any valid + * inline tag. + * + * @param {string} string - The string to check. + * @param {?string} tagName - The inline tag to match. May contain regexp characters. If this + * parameter is omitted, this method returns `true` for any valid inline tag. + * @returns {boolean} Set to `true` if the string is a valid inline tag or `false` in all other + * cases. + */ +exports.isInlineTag = function(string, tagName) { + try { + return regExpFactory(tagName, '^', '$').test(string); + } + catch(e) { + return false; + } +}; + /** * Replace all instances of multiple inline tags with other text. * @@ -61,7 +100,7 @@ exports.replaceInlineTags = function(string, replacers) { string = string || ''; Object.keys(replacers).forEach(function(replacer) { - var tagRegExp = new RegExp('\\{@' + replacer + '\\s+((?:.|\n)+?)\\}', 'gi'); + var tagRegExp = regExpFactory(replacer); var matches; // call the replacer once for each match while ( (matches = tagRegExp.exec(string)) !== null ) { @@ -69,7 +108,7 @@ exports.replaceInlineTags = function(string, replacers) { } }); - return { + return { tags: tagInfo, newString: string.trim() }; diff --git a/test/fixtures/requirestag.js b/test/fixtures/requirestag.js index 3fe27229..49b626fe 100644 --- a/test/fixtures/requirestag.js +++ b/test/fixtures/requirestag.js @@ -1,12 +1,20 @@ /** -* @requires module:foo/helper -*/ + * @requires module:foo/helper + */ function foo() { } /** -* @requires foo -* @requires Pez#blat this text is ignored -*/ + * @requires foo + * @requires Pez#blat this text is ignored + */ function bar() { } + +/** + * @requires {@link module:zest} + * @requires {@linkplain module:zing} + * @requires {@linkstupid module:pizzazz} + */ +function baz() { +} diff --git a/test/specs/jsdoc/tag/inline.js b/test/specs/jsdoc/tag/inline.js index de206a76..a89fb47d 100644 --- a/test/specs/jsdoc/tag/inline.js +++ b/test/specs/jsdoc/tag/inline.js @@ -12,6 +12,11 @@ describe('jsdoc/tag/inline', function() { expect(typeof jsdoc.tag.inline).toBe('object'); }); + it('should export an isInlineTag function', function() { + expect(jsdoc.tag.inline.isInlineTag).toBeDefined(); + expect(typeof jsdoc.tag.inline.isInlineTag).toBe('function'); + }); + it('should export a replaceInlineTag function', function() { expect(jsdoc.tag.inline.replaceInlineTag).toBeDefined(); expect(typeof jsdoc.tag.inline.replaceInlineTag).toBe('function'); @@ -22,6 +27,43 @@ describe('jsdoc/tag/inline', function() { expect(typeof jsdoc.tag.inline.replaceInlineTag).toBe('function'); }); + describe('isInlineTag', function() { + var isInlineTag = jsdoc.tag.inline.isInlineTag; + + it('should correctly identify an inline tag', function() { + expect( isInlineTag('{@mytag hooray}', 'mytag') ).toBe(true); + }); + + it('should correctly identify a non-inline tag', function() { + expect( isInlineTag('mytag hooray', 'mytag') ).toBe(false); + }); + + it('should report that a string containing an inline tag is not an inline tag', function() { + expect( isInlineTag('this is {@mytag hooray}', 'mytag') ).toBe(false); + }); + + it('should default to allowing any inline tag', function() { + expect( isInlineTag('{@anyoldtag will do}') ).toBe(true); + }); + + it('should still identify non-inline tags when a tag name is not provided', function() { + expect( isInlineTag('mytag hooray') ).toBe(false); + }); + + it('should allow regexp characters in the tag name', function() { + expect( isInlineTag('{@mytags hooray}', 'mytag\\S') ).toBe(true); + }); + + it('should return false (rather than throwing) with invalid input', function() { + function badInput() { + return isInlineTag({}); + } + + expect(badInput).not.toThrow(); + expect( badInput() ).toBe(false); + }); + }); + describe('replaceInlineTag', function() { it('should throw if the tag is matched and the replacer is invalid', function() { function badReplacerUndefined() { diff --git a/test/specs/tags/requirestag.js b/test/specs/tags/requirestag.js index 40eae164..f6bcf7ac 100644 --- a/test/specs/tags/requirestag.js +++ b/test/specs/tags/requirestag.js @@ -1,14 +1,24 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ describe("@requires tag", function() { - var docSet = jasmine.getDocSetFromFile('test/fixtures/requirestag.js'), - foo = docSet.getByLongname('foo')[0], - bar = docSet.getByLongname('bar')[0]; + var docSet = jasmine.getDocSetFromFile('test/fixtures/requirestag.js'); + var foo = docSet.getByLongname('foo')[0]; + var bar = docSet.getByLongname('bar')[0]; + var baz = docSet.getByLongname('baz')[0]; - it('When a symbol has an @requires tag, the doclet has a requires property that includes that value, with the "module:" namespace added.', function() { - expect(typeof foo.requires).toBe('object'); + it('When a symbol has a @requires tag, the doclet has a requires property that includes that value, with the "module:" namespace added.', function() { + expect( Array.isArray(foo.requires) ).toBe(true); expect(foo.requires[0]).toBe('module:foo/helper'); - expect(typeof bar.requires).toBe('object'); + expect( Array.isArray(bar.requires) ).toBe(true); expect(bar.requires[0]).toBe('module:foo'); expect(bar.requires[1]).toBe('module:Pez#blat'); }); -}); \ No newline at end of file + + it('When a symbol has a @requires tag whose value is an inline {@link} tag, the doclet has a requires property that includes that tag without modification.', function() { + expect( Array.isArray(baz.requires) ).toBe(true); + expect(baz.requires[0]).toBe('{@link module:zest}'); + expect(baz.requires[1]).toBe('{@linkplain module:zing}'); + // by design, we don't validate the tag name, as long as it starts with @link + expect(baz.requires[2]).toBe('{@linkstupid module:pizzazz}'); + }); +});