Merge pull request #10720 from MoonE/apidoc-better-search

Apidoc better search
This commit is contained in:
Andreas Hocevar
2020-02-28 14:27:20 +01:00
committed by GitHub
5 changed files with 383 additions and 168 deletions

View File

@@ -106,6 +106,20 @@ function includeTypes(doclet) {
}
}
const defaultExports = {};
const path = require('path');
const moduleRoot = path.join(process.cwd(), 'src');
// Tag default exported Identifiers because their name should be the same as the module name.
exports.astNodeVisitor = {
visitNode: function(node, e, parser, currentSourceName) {
if (node.type === 'Identifier' && node.parent.type === 'ExportDefaultDeclaration') {
const modulePath = path.relative(moduleRoot, currentSourceName).replace(/\.js$/, '');
defaultExports['module:' + modulePath + '~' + node.name] = true;
}
}
};
exports.handlers = {
newDoclet: function(e) {
@@ -169,6 +183,15 @@ exports.handlers = {
delete doclet.undocumented;
}
}
},
processingComplete(e) {
const byLongname = e.doclets.index.longname;
for (const name in defaultExports) {
byLongname[name].forEach(function(doclet) {
doclet.isDefaultExport = true;
});
}
}
};

View File

@@ -14,7 +14,6 @@ const path = require('jsdoc/lib/jsdoc/path');
const taffy = require('taffydb').taffy;
const handle = require('jsdoc/lib/jsdoc/util/error').handle;
const helper = require('jsdoc/lib/jsdoc/util/templateHelper');
const _ = require('underscore');
const htmlsafe = helper.htmlsafe;
const linkto = helper.linkto;
const resolveAuthorLinks = helper.resolveAuthorLinks;
@@ -188,10 +187,12 @@ function attachModuleSymbols(doclets, modules) {
});
}
function getPrettyName(longname) {
return longname
.split('~')[0]
.replace('module:', '');
function getPrettyName(doclet) {
const fullname = doclet.longname.replace('module:', '');
if (doclet.isDefaultExport) {
return fullname.split('~')[0];
}
return fullname;
}
/**
@@ -209,27 +210,13 @@ function getPrettyName(longname) {
*/
function buildNav(members) {
const nav = [];
// merge namespaces and classes, then sort
const merged = members.modules.concat(members.classes);
merged.sort(function(a, b) {
const prettyNameA = getPrettyName(a.longname).toLowerCase();
const prettyNameB = getPrettyName(b.longname).toLowerCase();
if (prettyNameA > prettyNameB) {
return 1;
}
if (prettyNameA < prettyNameB) {
return -1;
}
return 0;
});
_.each(merged, function(v) {
members.classes.forEach(function(v) {
// exclude interfaces from sidebar
if (v.interface !== true && v.kind === 'class') {
if (v.interface !== true) {
nav.push({
type: 'class',
longname: v.longname,
prettyname: getPrettyName(v.longname),
prettyname: getPrettyName(v),
name: v.name,
module: find({
kind: 'module',
@@ -253,44 +240,57 @@ function buildNav(members) {
memberof: v.longname
})
});
} else if (v.kind == 'module') {
const classes = find({
kind: 'class',
memberof: v.longname
});
const members = find({
kind: 'member',
memberof: v.longname
});
const methods = find({
kind: 'function',
memberof: v.longname
});
const typedefs = find({
kind: 'typedef',
memberof: v.longname
});
const events = find({
kind: 'event',
memberof: v.longname
});
// Only add modules that contain more than just classes with their
// associated Options typedef
if (typedefs.length > classes.length || members.length + methods.length > 0) {
nav.push({
type: 'module',
longname: v.longname,
prettyname: getPrettyName(v.longname),
name: v.name,
members: members,
methods: methods,
typedefs: typedefs,
fires: v.fires,
events: events
});
}
}
});
members.modules.forEach(function(v) {
const classes = find({
kind: 'class',
memberof: v.longname
});
const members = find({
kind: 'member',
memberof: v.longname
});
const methods = find({
kind: 'function',
memberof: v.longname
});
const typedefs = find({
kind: 'typedef',
memberof: v.longname
});
const events = find({
kind: 'event',
memberof: v.longname
});
// Only add modules that contain more than just classes with their
// associated Options typedef
if (typedefs.length > classes.length || members.length + methods.length > 0) {
nav.push({
type: 'module',
longname: v.longname,
prettyname: getPrettyName(v),
name: v.name,
members: members,
methods: methods,
typedefs: typedefs,
fires: v.fires,
events: events
});
}
});
nav.sort(function(a, b) {
const prettyNameA = a.prettyname.toLowerCase();
const prettyNameB = b.prettyname.toLowerCase();
if (prettyNameA > prettyNameB) {
return 1;
}
if (prettyNameA < prettyNameB) {
return -1;
}
return 0;
});
return nav;
}

View File

@@ -1,99 +1,258 @@
$(function () {
// Search Items
$('#include_modules').change(function (e) {
console.log('change');
if ($(this).is(':checked')) {
'use strict';
// Allow user configuration?
const allowRegex = true;
const minInputForSearch = 1;
const minInputForFullText = 2;
const expandAllOnInputWithoutSearch = true;
function constructRegex(searchTerm, makeRe, allowRegex) {
try {
if (allowRegex) {
return makeRe(searchTerm);
}
} catch (e) {
}
// In case of invalid regexp fall back to non-regexp, but still allow . to match /
return makeRe(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\./g, '[./]'));
}
function getWeightFunction(searchTerm, allowRegex) {
function makeRe(searchTerm) {
return {
begin: new RegExp('\\b' + searchTerm), // Begin matches word boundary
baseName: new RegExp('\\b' + searchTerm + '[^/]*$'), // Begin matches word boundary of class / module name
fullName: new RegExp('\\b' + searchTerm + '(?:[~.]|$)'), // Complete word(s) of class / module matches
completeName: new RegExp('^' + searchTerm + '$') // Match from start to finish
}
}
const re = constructRegex(searchTerm, makeRe, allowRegex);
return function (matchedItem, beginOnly) {
// We could get smarter on the weight here
const name = matchedItem.dataset.name;
if (beginOnly) {
return re.baseName.test(name) ? 100 : 1;
}
// If everything else is equal, prefer shorter names, and prefer classes over modules
let weight = 10000 + matchedItem.dataset.longname.length - name.length * 100;
if (name.match(re.begin)) {
weight += 10000;
if (re.baseName.test(name)) {
weight += 10000;
if (re.fullName.test(name)) {
weight += 10000;
if (re.completeName.test(name)) {
weight += 10000;
}
}
}
}
return weight;
}
}
const search = (function () {
const $navList = $('.navigation-list');
const navListNode = $navList.get(0);
let $classItems;
let $members;
let stateClass = (function () {
$navList.removeClass('search-started searching');
$navList.addClass('search-empty');
return 'search-empty';
})();
let manualToggles = {};
// Show an item related a current documentation automatically
const longname = $('.page-title').data('filename')
.replace(/\.[a-z]+$/, '')
.replace('module-', 'module:')
.replace(/_/g, '/')
.replace(/-/g, '~');
const currentItem = navListNode.querySelector('.item[data-longname="' + longname + '"]');
if (currentItem) {
$navList.prepend(currentItem);
}
return {
$navList: $navList,
$currentItem: currentItem ? $(currentItem) : undefined,
lastSearchTerm: undefined,
lastState: {},
getClassList: function () {
return $classItems || ($classItems = $navList.find('li.item'));
},
getMembers: function () {
return $members || ($members = $navList.find('.item li'));
},
changeStateClass: function (newClass) {
if (newClass !== stateClass) {
navListNode.classList.remove(stateClass);
navListNode.classList.add(newClass);
stateClass = newClass;
}
},
manualToggle: function ($node, show) {
$node.addClass('toggle-manual');
$node.toggleClass('toggle-manual-hide', !show);
$node.toggleClass('toggle-manual-show', show);
manualToggles[$node.data('longname')] = $node;
},
clearManualToggles: function() {
for (let clsName in manualToggles) {
manualToggles[clsName].removeClass('toggle-manual toggle-manual-show toggle-manual-hide');
}
manualToggles = {};
},
};
})();
const dummy = {subItems: {}};
function clearOldMatches(lastState, searchState) {
for (let itemName in lastState) {
const lastItem = lastState[itemName];
const item = searchState[itemName];
if (!item) {
lastItem.item.classList.remove('match');
}
if (lastItem.subItems) {
clearOldMatches(lastItem.subItems, (item || dummy).subItems);
}
}
}
function doSearch(searchTerm) {
searchTerm = searchTerm.toLowerCase();
const lastSearchTerm = search.lastSearchTerm;
if (searchTerm === lastSearchTerm) {
return;
}
// Avoid layout reflow by scrolling to top first.
search.$navList.scrollTop(0);
search.lastSearchTerm = searchTerm;
search.clearManualToggles();
if (searchTerm.length < minInputForSearch) {
const state = searchTerm.length && expandAllOnInputWithoutSearch ? 'search-started' : 'search-empty';
search.changeStateClass(state);
if (lastSearchTerm !== undefined && lastSearchTerm.length >= minInputForSearch) {
// Restore the original, sorted order
search.$navList.append(search.getClassList());
}
if (state === 'search-empty' && search.$currentItem) {
search.manualToggle(search.$currentItem, true);
}
} else {
}
});
var getSearchWeight = function (searchTerm, $matchedItem) {
let weight = 0;
// We could get smarter on the weight here
if ($matchedItem.data('shortname')
&& $matchedItem.data('shortname').toLowerCase() === searchTerm.toLowerCase()) {
weight++;
}
return weight;
};
// sort function callback
var weightSorter = function (a, b) {
var aW = $(a).data('weight') || 0;
var bW = $(b).data('weight') || 0;
return bW - aW;
};
// Search Items
$('#search').on('keyup', function (e) {
var value = $(this).val();
var $el = $('.navigation');
if (value && value.length > 1) {
var regexp = new RegExp(value, 'i');
$el.find('li, .member-list').hide();
$el.find('li').each(function (i, v) {
const $item = $(v);
const name = $item.data('name');
if (name && regexp.test(name)) {
const $classEntry = $item.closest('.item');
const $members = $item.closest('.member-list');
// Do the weight thing
$classEntry.removeData('weight');
$classEntry.show();
const weight = getSearchWeight(value, $classEntry);
$classEntry.data('weight', weight);
$members.show();
$classEntry.show();
$item.show();
search.changeStateClass('searching');
searchTerm = searchTerm.toLowerCase();
const beginOnly = searchTerm.length < minInputForFullText;
const getSearchWeight = getWeightFunction(searchTerm, allowRegex);
const re = constructRegex(searchTerm, function (searchTerm) {
return new RegExp((beginOnly ? '\\b' : '') + searchTerm);
}, allowRegex);
const navList = search.$navList.get(0);
const classes = [];
const searchState = {};
search.getClassList().each(function (i, classEntry) {
const className = classEntry.dataset.longname;
if (!(className in searchState) && re.test(classEntry.dataset.name)) {
const cls = searchState[className] = {
item: classEntry,
// Do the weight thing
weight: getSearchWeight(classEntry, beginOnly) * 100000,
subItems: {}
};
classes.push(cls);
classEntry.classList.add('match');
}
});
search.getMembers().each(function (i, li) {
const name = li.dataset.name;
if (re.test(name)) {
const itemMember = li.parentElement.parentElement;
const classEntry = itemMember.parentElement;
const className = classEntry.dataset.longname;
let cls = searchState[className];
if (!cls) {
cls = searchState[className] = {
item: classEntry,
weight: 0,
subItems: {}
};
classes.push(cls);
classEntry.classList.add('match');
}
cls.weight += getSearchWeight(li, true);
const memberType = itemMember.dataset.type;
let members = cls.subItems[memberType];
if (!members) {
members = cls.subItems[memberType] = {
item: itemMember,
subItems: {}
};
itemMember.classList.add('match');
}
members.subItems[name] = { item: li };
li.classList.add('match');
}
});
clearOldMatches(search.lastState, searchState);
search.lastState = searchState;
$(".navigation ul.list li.item:visible")
.sort(weightSorter) // sort elements
.appendTo(".navigation ul.list"); // append again to the list
} else {
$el.find('.item, .member-list').show();
classes.sort(function (a, b) {
return a.weight - b.weight;
});
for (let i = classes.length - 1; i >= 0; --i) {
navList.appendChild(classes[i].item);
}
}
}
$el.find('.list').scrollTop(0);
});
const searchInput = $('#search').get(0);
// Skip searches when typing fast.
let key;
function queueSearch() {
if (!key) {
key = setTimeout(function () {
key = undefined;
const searchTerm = searchInput.value;
doSearch(searchTerm);
}, 0);
}
}
// Search Items
searchInput.addEventListener('input', queueSearch);
doSearch(searchInput.value);
// Toggle when click an item element
$('.navigation').on('click', '.toggle', function (e) {
$(this).parent().parent().find('.member-list').toggle();
search.$navList.on('click', '.toggle', function (e) {
if (event.target.tagName.toLowerCase() === 'a') {
return;
}
const clsItem = $(this).closest('.item');
let show;
if (clsItem.hasClass('toggle-manual-show')) {
show = false;
} else if (clsItem.hasClass('toggle-manual-hide')) {
show = true;
} else {
clsItem.find('.member-list li').each(function (i, v) {
show = $(v).is(':hidden');
return !show;
});
}
search.manualToggle(clsItem, !!show);
});
// Show an item related a current documentation automatically
var filename = $('.page-title').data('filename')
.replace(/\.[a-z]+$/, '')
.replace('module-', 'module:')
.replace(/_/g, '/')
.replace(/-/g, '~');
var $currentItem = $('.navigation .item[data-name*="' + filename + '"]:eq(0)');
if ($currentItem.length) {
$currentItem
.remove()
.prependTo('.navigation .list')
.show()
.find('.member-list')
.show();
}
// Auto resizing on navigation
var _onResize = function () {
var height = $(window).height();
var $el = $('.navigation');
$el.height(height).find('.list').height(height - 133);
$el.height(height).find('.navigation-list').height(height - 133);
};
$(window).on('resize', _onResize);
@@ -137,22 +296,4 @@ $(function () {
'<a href="' + link + textParts[1].replace('line ', '#L') + '">' +
textParts[1] + '</a>';
});
// Highlighting current anchor
var anchors = $('.anchor');
var _onHashChange = function () {
var activeHash = window.document.location.hash
.replace(/\./g, '\\.') // Escape dot in element id
.replace(/\~/g, '\\~'); // Escape tilde in element id
anchors.removeClass('highlighted');
if (activeHash.length > 0) {
anchors.filter(activeHash).addClass('highlighted');
}
};
$(window).on('hashchange', _onHashChange);
_onHashChange();
});

View File

@@ -51,7 +51,8 @@ body {
width: 0px;
height: 0px;
}
.nameContainer .anchor.highlighted + h4 {
/* Highlighting current anchor */
.nameContainer .anchor:target + h4 {
background-color: #faebcc;
}
a {
@@ -123,7 +124,7 @@ li {
color: #fff;
border-color: #555;
}
.navigation .list {
.navigation .navigation-list {
padding: 10px 15px 0 15px;
position: relative;
overflow: auto;
@@ -135,6 +136,24 @@ li {
border-bottom: 1px solid #333;
}
.navigation .glyphicon {
margin-right: 3px;
flex-shrink: 0;
}
.navigation .item .glyphicon:before {
display: inline-block;
}
.navigation .item.toggle-manual .glyphicon:before {
transition: transform .1s;
}
.navigation .item-class.toggle-manual-show .glyphicon:before {
/* With 90deg the icon slightly slides left at transition end */
transform: rotate(89.9deg);
}
.navigation .item-module.toggle-manual-show .glyphicon:before {
transform: rotate(45deg);
}
.navigation li.perfect-match {
border: 5px solid orange;
}
@@ -147,8 +166,8 @@ li {
}
.navigation li.item .title {
cursor: pointer;
position: relative;
display: block;
display: -ms-flexbox;
display: flex;
font-size: 0.85em;
}
.navigation li.item .title a {
@@ -184,9 +203,41 @@ li {
margin-top: 2px;
}
.navigation li.item .member-list {
display: none;
padding-left: 8px;
}
/* search state */
/* show all classes when search is empty */
.navigation-list.search-empty .item {
display: block;
}
/* hide all members by default when search is empty */
.navigation-list.search-empty .item .member-list {
display: none;
}
/* expand all members when input in search field available but too short to search */
.navigation-list.search-started li,
.navigation-list.search-started .member-list {
display: block;
}
/* when searching hide everything that is not a match */
.navigation-list.searching li,
.navigation-list.searching .member-list {
display: none;
}
.navigation-list.searching .match {
display: block;
}
/* allow user to hide / show members */
.navigation-list .item.toggle-manual-show li,
.navigation-list .item.toggle-manual-show .member-list {
display: block!important;
}
.navigation-list:not(.searching) .item.toggle-manual-hide li,
.navigation-list:not(.searching) .item.toggle-manual-hide .member-list {
display: none!important;
}
.main {
padding: 20px 20px;
margin-left: 250px;

View File

@@ -15,17 +15,17 @@ function getItemCssClass(type) {
}
const printList = v => { ?>
<li data-name="<?js= v.longname ?>"><?js
<li data-name="<?js= toShortName(v.name).toLowerCase() ?>"><?js
}
const printListWithStability = v => {
const cls = v.stability && v.stability !== 'stable' ? ' class="unstable"' : ''; ?>
<li data-name="<?js= v.longname ?>"<?js= cls ?>><?js
<li data-name="<?js= toShortName(v.name).toLowerCase() ?>"<?js= cls ?>><?js
}
function listContent(item, title, listItemPrinter) {
const type = title.toLowerCase();
if (item[type] && item[type].length) { ?>
<div class="member-list">
<div class="member-list" data-type="<?js= type ?>">
<span class="subtitle"><?js= title ?></span>
<ul><?js
item[type].forEach(function (v) {
@@ -40,12 +40,12 @@ function listContent(item, title, listItemPrinter) {
<div class="search">
<input id="search" type="text" class="form-control input-sm" placeholder="Search Documentation">
</div>
<ul class="list"><?js
<ul class="navigation-list search-empty"><?js
this.nav.forEach(function (item) { ?>
<li class="item" data-name="<?js= item.longname ?>" data-shortname="<?js= item.name.toLowerCase() ?>">
<span class="title">
<span class="glyphicon <?js= getItemCssClass(item.type) ?> toggle"></span>
<?js= self.linkto(item.longname, item.prettyname) ?>
<li class="item item-<?js= item.type ?>" data-longname="<?js= item.longname ?>" data-name="<?js= item.prettyname.toLowerCase() ?>">
<span class="title toggle">
<span class="glyphicon <?js= getItemCssClass(item.type) ?>"></span>
<span><?js= self.linkto(item.longname, item.prettyname.replace(/[.~]/g, '\u200b$&')) ?></span>
</span><?js
listContent(item, 'Members', printList);
listContent(item, 'Typedefs', printListWithStability);