diff --git a/lib/jsdoc/util/templateHelper.js b/lib/jsdoc/util/templateHelper.js
index 035e85ad..fe96e1d3 100644
--- a/lib/jsdoc/util/templateHelper.js
+++ b/lib/jsdoc/util/templateHelper.js
@@ -32,6 +32,33 @@ exports.globalName = name.SCOPE.NAMES.GLOBAL;
exports.fileExtension = '.html';
exports.scopeToPunc = name.scopeToPunc;
+var linkMap = {
+ // two-way lookup
+ longnameToUrl: {},
+ urlToLongname: {},
+
+ // one-way lookup (IDs are only unique per file)
+ longnameToId: {}
+};
+
+// two-way lookup
+var tutorialLinkMap = {
+ nameToUrl: {},
+ urlToName: {}
+};
+
+var longnameToUrl = exports.longnameToUrl = linkMap.longnameToUrl;
+var longnameToId = exports.longnameToId = linkMap.longnameToId;
+
+var registerLink = exports.registerLink = function(longname, fileUrl) {
+ linkMap.longnameToUrl[longname] = fileUrl;
+ linkMap.urlToLongname[fileUrl] = longname;
+};
+
+var registerId = exports.registerId = function(longname, fragment) {
+ linkMap.longnameToId[longname] = fragment;
+};
+
function getNamespace(kind) {
if (dictionary.isNamespace(kind)) {
return kind + ':';
@@ -39,6 +66,20 @@ function getNamespace(kind) {
return '';
}
+function formatNameForLink(doclet, options) {
+ var newName = getNamespace(doclet.kind) + (doclet.name || '');
+ var scopePunc = exports.scopeToPunc[doclet.scope] || '';
+
+ // Only prepend the scope punctuation if it's not the same character that marks the start of a
+ // fragment ID. Using `#` in HTML5 fragment IDs is legal, but URLs like `foo.html##bar` are
+ // just confusing.
+ if (scopePunc !== '#') {
+ newName = scopePunc + newName;
+ }
+
+ return newName;
+}
+
function makeUniqueFilename(filename, str) {
var key = filename.toLowerCase();
var nonUnique = true;
@@ -63,32 +104,6 @@ function makeUniqueFilename(filename, str) {
return filename;
}
-function makeUniqueId(filename, id) {
- var key = id.toLowerCase();
- var nonUnique = true;
-
- // append enough underscores to make the identifier unique
- while (nonUnique) {
- if ( hasOwnProp.call(ids, filename) && ids[filename].indexOf(key) !== -1 ) {
- id += '_';
- key = id.toLowerCase();
- }
- else {
- nonUnique = false;
- }
- }
-
- ids[filename] = ids[filename] || [];
- ids[filename].push(id);
-
- return id;
-}
-
-var htmlsafe = exports.htmlsafe = function(str) {
- return str.replace(/&/g, '&')
- .replace(/$/g, '') : '';
if ( hasUrlPrefix(stripped) ) {
- url = stripped;
+ fileUrl = stripped;
text = linkText || stripped;
}
// handle complex type expressions that may require multiple links
@@ -235,18 +323,18 @@ function buildLink(longname, linkText, options) {
return stringifyType(parsedType, options.cssClass, options.linkMap);
}
else {
- url = hasOwnProp.call(options.linkMap, longname) ? options.linkMap[longname] : '';
+ fileUrl = hasOwnProp.call(options.linkMap, longname) ? options.linkMap[longname] : '';
text = linkText || longname;
}
text = options.monospace ? '' + text + '' : text;
- if (!url) {
+ if (!fileUrl) {
return text;
}
else {
- return util.format('%s', encodeURI(url + fragmentString), classString,
- text);
+ return util.format('%s', encodeURI(fileUrl + fragmentString),
+ classString, text);
}
}
@@ -328,19 +416,20 @@ function splitLinkText(text) {
}
var tutorialToUrl = exports.tutorialToUrl = function(tutorial) {
+ var fileUrl;
var node = tutorials.getByName(tutorial);
+
// no such tutorial
if (!node) {
require('jsdoc/util/logger').error( new Error('No such tutorial: ' + tutorial) );
return null;
}
- var url;
// define the URL if necessary
if (!hasOwnProp.call(tutorialLinkMap.nameToUrl, node.name)) {
- url = 'tutorial-' + getUniqueFilename(node.name);
- tutorialLinkMap.nameToUrl[node.name] = url;
- tutorialLinkMap.urlToName[url] = node.name;
+ fileUrl = 'tutorial-' + getUniqueFilename(node.name);
+ tutorialLinkMap.nameToUrl[node.name] = fileUrl;
+ tutorialLinkMap.urlToName[fileUrl] = node.name;
}
return tutorialLinkMap.nameToUrl[node.name];
@@ -480,20 +569,6 @@ var find = exports.find = function(data, spec) {
return data(spec).get();
};
-/**
- * Check whether a symbol is the only symbol exported by a module (as in
- * `module.exports = function() {};`).
- *
- * @private
- * @param {module:jsdoc/doclet.Doclet} doclet - The doclet for the symbol.
- * @return {boolean} `true` if the symbol is the only symbol exported by a module; otherwise,
- * `false`.
- */
-function isModuleExports(doclet) {
- return doclet.longname && doclet.longname === doclet.name &&
- doclet.longname.indexOf(MODULE_NAMESPACE) === 0 && doclet.kind !== 'module';
-}
-
/**
* Retrieve all of the following types of members from a set of doclets:
*
@@ -771,38 +846,25 @@ exports.prune = function(data) {
return data;
};
-var registerLink = exports.registerLink = function(longname, url) {
- linkMap.longnameToUrl[longname] = url;
- linkMap.urlToLongname[url] = longname;
-};
-
/**
- * Get a longname's filename if one has been registered; otherwise, generate a unique filename, then
- * register the filename.
- * @private
+ * Create a URL that points to the generated documentation for the doclet.
+ *
+ * If a doclet corresponds to an output file (for example, if the doclet represents a class), the
+ * URL will consist of a filename.
+ *
+ * If a doclet corresponds to a smaller portion of an output file (for example, if the doclet
+ * represents a method), the URL will consist of a filename and a fragment ID.
+ *
+ * @param {module:jsdoc/doclet.Doclet} doclet - The doclet that will be used to create the URL.
+ * @return {string} The URL to the generated documentation for the doclet.
*/
-function getFilename(longname) {
- var url;
-
- if ( longnameToUrl[longname] && hasOwnProp.call(longnameToUrl, longname) ) {
- url = longnameToUrl[longname];
- } else {
- url = getUniqueFilename(longname);
- registerLink(longname, url);
- }
-
- return url;
-}
-
-/** Turn a doclet into a URL. */
exports.createLink = function(doclet) {
- var filename;
- var fragment;
- var match;
var fakeContainer;
-
- var url = '';
+ var filename;
+ var fileUrl;
+ var fragment = '';
var longname = doclet.longname;
+ var match;
// handle doclets in which doclet.longname implies that the doclet gets its own HTML file, but
// doclet.kind says otherwise. this happens due to mistagged JSDoc (for example, a module that
@@ -822,22 +884,23 @@ exports.createLink = function(doclet) {
// mistagged version of a doclet that gets its own HTML file
else if ( containers.indexOf(doclet.kind) === -1 && fakeContainer ) {
filename = getFilename(doclet.memberof || longname);
- if (doclet.name === doclet.longname) {
- fragment = '';
- }
- else {
- fragment = doclet.name || '';
+ if (doclet.name !== doclet.longname) {
+ fragment = formatNameForLink(doclet);
+ fragment = getId(longname, fragment);
}
}
// the doclet is within another HTML file
else {
filename = getFilename(doclet.memberof || exports.globalName);
- fragment = getNamespace(doclet.kind) + (doclet.name || '');
+ if ( (doclet.name !== doclet.longname) || (doclet.scope === name.SCOPE.NAMES.GLOBAL) ) {
+ fragment = formatNameForLink(doclet);
+ fragment = getId(longname, fragment);
+ }
}
- url = encodeURI( filename + fragmentHash(fragment) );
+ fileUrl = encodeURI( filename + fragmentHash(fragment) );
- return url;
+ return fileUrl;
};
// TODO: docs
diff --git a/test/specs/jsdoc/util/templateHelper.js b/test/specs/jsdoc/util/templateHelper.js
index 940bb5eb..1e384dd7 100644
--- a/test/specs/jsdoc/util/templateHelper.js
+++ b/test/specs/jsdoc/util/templateHelper.js
@@ -265,8 +265,48 @@ describe("jsdoc/util/templateHelper", function() {
});
});
- xdescribe('getUniqueId', function() {
- // TODO
+ describe('getUniqueId', function() {
+ it('should return the provided string in normal cases', function() {
+ var id = helper.getUniqueId('futon.html', 'backrest');
+ expect(id).toBe('backrest');
+ });
+
+ it('should return an empty string if no base ID is provided', function() {
+ var id = helper.getUniqueId('futon.html', '');
+ expect(id).toBe('');
+ });
+
+ it('should remove whitespace characters', function() {
+ var id = helper.getUniqueId('futon.html', 'a very long identifier');
+ expect(id).toBe('averylongidentifier');
+ });
+
+ it('should not return the same ID twice for a given file', function() {
+ var filename = 'futon.html';
+ var name = 'polymorphic';
+ var id1 = helper.getUniqueId(filename, name);
+ var id2 = helper.getUniqueId(filename, name);
+
+ expect(id1).not.toBe(id2);
+ });
+
+ it('should allow duplicate IDs if they are in different files', function() {
+ var name = 'magnificence';
+ var id1 = helper.getUniqueId('supersensational.html', name);
+ var id2 = helper.getUniqueId('razzledazzle.html', name);
+
+ expect(id1).toBe(id2);
+ });
+
+ it('should not consider the same name with different letter case to be unique', function() {
+ var camel = 'myJavaScriptIdentifier';
+ var pascal = 'MyJavaScriptIdentifier';
+ var filename = 'mercutio.html';
+ var id1 = helper.getUniqueId(filename, camel);
+ var id2 = helper.getUniqueId(filename, pascal);
+
+ expect( id1.toLowerCase() ).not.toBe( id2.toLowerCase() );
+ });
});
describe("longnameToUrl", function() {
@@ -1356,7 +1396,8 @@ describe("jsdoc/util/templateHelper", function() {
var mockDoclet = {
kind: 'function',
longname: 'foo',
- name: 'foo'
+ name: 'foo',
+ scope: 'global'
},
url = helper.createLink(mockDoclet);
@@ -1468,6 +1509,45 @@ describe("jsdoc/util/templateHelper", function() {
expect(badModuleDocletUrl).toBe('module-qux.html');
expect(memberDocletUrl).toBe('module-qux.html#frozzle');
});
+
+ it('should include the scope punctuation in the fragment ID for static members', function() {
+ var functionDoclet = {
+ kind: 'function',
+ longname: 'Milk.pasteurize',
+ name: 'pasteurize',
+ memberof: 'Milk',
+ scope: 'static'
+ };
+ var docletUrl = helper.createLink(functionDoclet);
+
+ expect(docletUrl).toBe('Milk.html#.pasteurize');
+ });
+
+ it('should include the scope punctuation in the fragment ID for inner members', function() {
+ var functionDoclet = {
+ kind: 'function',
+ longname: 'Milk~removeSticksAndLeaves',
+ name: 'removeSticksAndLeaves',
+ memberof: 'Milk',
+ scope: 'inner'
+ };
+ var docletUrl = helper.createLink(functionDoclet);
+
+ expect(docletUrl).toBe('Milk.html#~removeSticksAndLeaves');
+ });
+
+ it('should omit the scope punctuation from the fragment ID for instance members', function() {
+ var propertyDoclet = {
+ kind: 'member',
+ longname: 'Milk#calcium',
+ name: 'calcium',
+ memberof: 'Milk',
+ scope: 'instance'
+ };
+ var docletUrl = helper.createLink(propertyDoclet);
+
+ expect(docletUrl).toBe('Milk.html#calcium');
+ });
});
describe("resolveAuthorLinks", function() {