docs: Better support for module name collisions

In /docs/modules/, show both modules if module IDs are not globally unique.
Show both modules' docs on same page in expandable containers.

In /docs/json/, both modules appear in lists in the doc, but disambiguating them isn't implemented yet.
This commit is contained in:
Matthew Holt 2021-06-02 15:16:15 -06:00
parent 5c3a67692e
commit e132c5c454
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
13 changed files with 265 additions and 126 deletions

View file

@ -18,9 +18,9 @@
<div class="article-container">
<div class="paper" id="paper1"></div>
<div class="paper" id="paper2"></div>
<article class="paper paper3">
{{markdown $markdownFile.Body}}
</article>
<div class="paper paper3">
<article>{{markdown $markdownFile.Body}}</article>
</div>
</div>
<div class="sidebar"></div>
</main>

View file

@ -15,13 +15,15 @@
<div class="article-container">
<div class="paper" id="paper1"></div>
<div class="paper" id="paper2"></div>
<article class="paper paper3">
<div class="breadcrumbs">
<!--Populated by JS-->
</div>
{{include "/includes/docs-renderbox.html"}}
{{include "/includes/docs-details.html"}}
</article>
<div class="paper paper3">
<article id="json-docs-container">
<div class="breadcrumbs">
<!--Populated by JS-->
</div>
{{include "/includes/docs-renderbox.html"}}
{{include "/includes/docs-details.html"}}
</article>
</div>
</div>
<div class="sidebar"></div>
</main>

View file

@ -15,27 +15,38 @@
<div class="article-container">
<div class="paper" id="paper1"></div>
<div class="paper" id="paper2"></div>
<article id="module-list-container" class="paper paper3">
<h1>All Modules</h1>
<p>
This page lists all registered Caddy modules.
</p>
<p>
We recommend using your browser's "Find in page" feature for quick lookups.
</p>
<table id="module-list">
<tr>
<th>Module ID</th>
<th>Description</th>
</tr>
<!--Populated by JS-->
</table>
</article>
<article id="module-docs-container" class="paper paper3">
<h1 id="module-name"><!--Populated by JS--></h1>
{{include "/includes/docs-renderbox.html"}}
{{include "/includes/docs-details.html"}}
</article>
<div class="paper paper3">
<article id="module-list-container">
<h1>All Modules</h1>
<p>
This page lists all registered Caddy modules. Modules are plugins which extend Caddy's <a href="/docs/json/">JSON configuration structure</a>.
</p>
<p>
We recommend using your browser's "Find in page" feature for quick lookups.
</p>
<table id="module-list">
<tr>
<th></th>
<th>Module ID</th>
<th>Description</th>
</tr>
<!--Populated by JS-->
</table>
</article>
<div id="module-docs-container">
<div class="pad"><h1 class="module-name"><!--Populated by JS--></h1></div>
<div id="module-multiple-repos">
There is more than one module named <b class="module-name"><!--Populated by JS--></b>. Choose one by its repository.
</div>
<div id="module-template" class="module-repo-container">
<div class="module-repo-selector"></div>
<article>
{{include "/includes/docs-renderbox.html"}}
{{include "/includes/docs-details.html"}}
</article>
</div>
</div>
</div>
</div>
<div class="sidebar"></div>
</main>

View file

@ -57,7 +57,7 @@
</div>
<div>
<div>
<b>Standard features:</b> &#9745;&#65039;
<b>Standard features:</b> <span title="All official Caddy builds come with standard plugins">&#9745;&#65039;</span>
</div>
</div>
<div>

View file

@ -2,15 +2,17 @@
<div class="nonstandard-notice">
<b>This module does not come with Caddy.</b> It can be added by using <b><a href="/docs/build#xcaddy">xcaddy</a></b> or our <b><a href="/download">download page</a></b>. Non-standard modules may be developed by the community and are not officially endorsed or maintained by the Caddy project. The documentation is shown here only as a courtesy.
<br><br>
<b>Module repository: <a href="javascript:" class="nonstandard-project-link"></a></b>
<b>Code repository: <a href="javascript:" class="nonstandard-project-link"></a></b>
<br><br>
<b>Custom builds:</b> <code class="bash">xcaddy build --with <span class="nonstandard-package-path"></span></code>
</div>
</div>
<h2 id="docs">Description</h2>
<div id="top-doc">
<div class="top-doc">
<!--Populated by JS-->
</div>
<h2 id="field-list-header">Field List</h2>
<dl id="field-list-contents">
<h2 class="field-list-header">Field List</h2>
<dl class="field-list-contents">
<!--Populated by JS-->
</dl>

View file

@ -1 +1 @@
<pre><code class="json" id="renderbox"><!--Populated by JS--></code></pre>
<pre><code class="json renderbox"><!--Populated by JS--></code></pre>

View file

@ -6,7 +6,7 @@ article h1 {
padding-top: 8%;
}
#renderbox {
.renderbox {
border-radius: 0;
font-size: 20px;
line-height: 1.6em;

View file

@ -224,6 +224,10 @@ main nav li li a {
max-width: 1100px;
}
.pad {
padding-top: 8%;
}
article {
padding-top: 8%;
padding-bottom: 8%;
@ -238,7 +242,8 @@ width, a few elements should be allowed to
extend to the borders of the page
*/
article > :not(.fullwidth),
article > .fullwidth > * {
article > .fullwidth > *,
.pad {
padding-left: 8%;
padding-right: 8%;
}
@ -448,13 +453,13 @@ iframe {
padding: 10px;
color: #886c00;
line-height: 1.4em;
display: none;
}
.nonstandard {
color: rgb(214, 145, 16);
}
.standard-flag,
.nonstandard-flag {
cursor: help;
font-size: 8px;
@ -462,10 +467,25 @@ iframe {
padding: 4px 8px;
text-transform: uppercase;
font-weight: bold;
background-color: rgb(238, 167, 34);
color: white;
border-radius: 4px;
white-space: nowrap;
vertical-align: middle;
}
.standard-flag {
background-color: rgb(34, 163, 23);
}
.nonstandard-flag {
background-color: rgb(238, 167, 34);
}
.module-repo-differentiator {
font-size: 11px;
font-weight: bold;
color: #555;
line-height: 1em;
}
@ -706,7 +726,8 @@ td code {
#module-docs-container,
#module-list-container {
#module-list-container,
#module-template {
display: none;
}
@ -729,7 +750,27 @@ td code {
}
#module-multiple-repos {
display: none;
margin: 25px;
}
.module-repo-selector {
font-size: 26px;
padding: 25px;
font-weight: bold;
border-bottom: 1px solid #888;
cursor: pointer;
}
.module-repo-selector:hover {
color: #009cda;
background: rgb(246, 250, 252);
}
.module-repo-selector-arrow {
margin: 10px;
}

View file

@ -1,13 +1,14 @@
// TODO: sanitize all HTML renderings, especially markdown: https://github.com/cure53/DOMPurify
var pageData = {}, pageDocs = {};
var $renderbox, $hovercard;
var pageDocs = {};
var pageData = {};
var $hovercard;
const nonStandardFlag = '<span class="nonstandard-flag" title="This module does not come with official Caddy distributions by default; it needs to be added to custom Caddy builds.">Non-standard</span>';
const standardFlag = '<span class="standard-flag" title="This module comes with official Caddy distributions by default.">Standard</span>';
$(function() {
$renderbox = $('#renderbox');
$hovercard = $('#hovercard');
var hoverTimeout;
@ -19,7 +20,7 @@ $(function() {
});
// toggle an object as expanded or collapsed
$('#renderbox').on('click', '.toggle-obj', function() {
$('body').on('click', '.renderbox .toggle-obj', function() {
if ($(this).hasClass('expanded')) {
// collapse
$(this).html('&#9656;');
@ -37,6 +38,8 @@ $(function() {
clearTimeout(hoverTimeout);
var pos = $(this).offset();
var moduleID = $(this).closest('.module-repo-container').data('module-id') || '';
var moduleData = pageData[moduleID];
// there is a gap between the hoverbox and the link that originated it;
// there may be a different link in this gap; if the hover box is visible,
@ -59,10 +62,10 @@ $(function() {
if ($(this).hasClass('module')) {
// module
var $list =$('<div/>');
if (pageData.namespaces && pageData.namespaces[modNamespace]) {
for (var i = 0; i < pageData.namespaces[modNamespace].length; i++) {
var modInfo = pageData.namespaces[modNamespace][i];
var href = canTraverse() ? '.'+elemPath+'/'+modInfo.name+'/' : './'+modNamespace+'.'+modInfo.name;
if (moduleData.namespaces && moduleData.namespaces[modNamespace]) {
for (var i = 0; i < moduleData.namespaces[modNamespace].length; i++) {
var modInfo = moduleData.namespaces[modNamespace][i];
var href = canTraverse(moduleData) ? '.'+elemPath+'/'+modInfo.name+'/' : './'+modNamespace+'.'+modInfo.name;
var content = '<a href="'+href+'" class="module-link"> '+modInfo.name;
if (!isStandard(modInfo.package)) {
content += nonStandardFlag;
@ -83,7 +86,7 @@ $(function() {
// breadcrumb siblings
var siblingPath = $(this).data('sibling-path');
var bcVal = pageData.breadcrumb[siblingPath];
var bcVal = moduleData.breadcrumb[siblingPath];
var bcSiblings = [];
// drill down to the true underlying type
@ -101,8 +104,8 @@ $(function() {
case "module":
case "module_map":
for (var j = 0; j < pageData.namespaces[bcVal.module_namespace].length; j++) {
var mod = pageData.namespaces[bcVal.module_namespace][j];
for (var j = 0; j < moduleData.namespaces[bcVal.module_namespace].length; j++) {
var mod = moduleData.namespaces[bcVal.module_namespace][j];
bcSiblings.push({name: mod.name, path: siblingPath, isStandard: isStandard(mod.package)})
}
}
@ -154,46 +157,58 @@ $(function() {
}, '.has-popup');
});
function beginRendering(json, moduleID) {
pageData = json;
console.log("PAGE DATA:", pageData);
function beginRenderingInto($tpl, moduleID, module) {
console.log("RENDERING:", moduleID, module);
$tpl.data('module-id', moduleID);
pageData[moduleID] = module;
// show notice if module is non-standard
if (pageData.repo && !isStandard(pageData.structure.type_name)) {
$('.nonstandard-project-link').attr('href', pageData.repo).text(pageData.repo);
$('.nonstandard-notice').prepend(nonStandardFlag).show();
if (module.repo) {
if (isStandard(module.structure.type_name)) {
$('.nonstandard-notice', $tpl).remove();
} else {
let { pkg, _ } = splitTypeName(module.structure.type_name);
$('.nonstandard-project-link', $tpl).attr('href', module.repo).text(module.repo);
$('.nonstandard-package-path', $tpl).text(pkg);
$('.nonstandard-notice', $tpl).prepend(nonStandardFlag);
}
var $repoName = $('<span/>').text(stripScheme(module.repo));
$('.module-repo-selector', $tpl).html('<span class="module-repo-selector-arrow">&#9656;</span>').append($repoName);
}
// for most types, just render their docs; but for maps or arrays, fall through to underlying type for docs
let rawDocs = pageData.structure.doc ?? pageData.structure.elems;
let rawDocs = module.structure.doc ?? module.structure.elems;
$('#top-doc').html(markdown(replaceGoTypeNameWithCaddyModuleName(rawDocs, moduleID)));
$('#top-doc').append(makeSubmoduleList("", pageData.structure));
$('.top-doc', $tpl).html(markdown(replaceGoTypeNameWithCaddyModuleName(rawDocs, module, moduleID)));
$('.top-doc', $tpl).append(makeSubmoduleList(module, "", module.structure));
renderData(pageData.structure, 0, "", $('<div class="group"/>'));
let $group = newGroup();
renderData($tpl, module, module.structure, 0, "", $group);
$('.renderbox', $tpl).append($group);
if ($('#field-list-contents').text().trim()) {
$('#field-list-header').show();
if ($('.field-list-contents', $tpl).text().trim()) {
$('.field-list-header', $tpl).show();
}
// if the browser tried to navigate directly to an element
// on the page when it loaded, it would have failed since
// we hadn't rendered it yet; but now we can scroll to it
// directly since rendering has finished
if (window.location.hash) {
window.location.hash = window.location.hash;
}
// TODO: see about fixing this for module and JSON docs pages
// // if the browser tried to navigate directly to an element
// // on the page when it loaded, it would have failed since
// // we hadn't rendered it yet; but now we can scroll to it
// // directly since rendering has finished
// if (window.location.hash.length > 1) {
// document.getElementById(window.location.hash.substr(1)).scrollIntoView();
// }
}
function renderData(data, nesting, path, $group) {
function renderData($tpl, module, data, nesting, path, $group) {
switch (data.type) {
case "struct":
$group.append('{<a href="javascript:" class="toggle-obj expanded" title="Collapse/expand">&#9662;</a>');
nesting++;
var $fieldGroup = $('<div class="group"/>');
renderModuleInlineKey(data, nesting, $fieldGroup);
var $fieldGroup = newGroup();
renderModuleInlineKey($tpl, module, data, nesting, $fieldGroup);
$group.append($fieldGroup);
if (data.struct_fields) {
// TODO: Not sure if sorting the struct fields is a good idea...
@ -216,19 +231,19 @@ function renderData(data, nesting, path, $group) {
// render the docs to the page
var fieldDoc = markdown(field.doc) || '<p class="explain">There are no docs for this property.</p>';
fieldDoc += makeSubmoduleList(fieldPath, field.value);
appendToFieldDocs(cleanFieldPath, fieldDoc);
fieldDoc += makeSubmoduleList(module, fieldPath, field.value);
appendToFieldDocs($tpl, module, cleanFieldPath, fieldDoc);
// render the field to the JSON box
var $fieldGroup = $('<div class="group"/>');
var $fieldGroup = newGroup();
indent(nesting, $fieldGroup);
var keyATag = '<a ';
if (canTraverse()) {
if (canTraverse(module)) {
keyATag += 'href=".'+fieldPath+'/" ';
}
keyATag += 'data-path="'+fieldPath+'" class="'+linkClass+'">'+field.key+'</a>';
$fieldGroup.append('<span class="qu">"</span><span class="key">'+keyATag+'</span><span class="qu">"</span>: ');
renderData(field.value, nesting, fieldPath, $fieldGroup);
renderData($tpl, module, field.value, nesting, fieldPath, $fieldGroup);
if (i < data.struct_fields.length-1) {
$fieldGroup.append(',');
}
@ -260,7 +275,7 @@ function renderData(data, nesting, path, $group) {
if (data.elems.type == "module_map") {
$group.append('{<a href=".'+path+'/" class="module has-popup" data-namespace="'+(data.elems.module_namespace || '')+'" data-path="'+path+'">&bull;&bull;&bull;</a>}');
} else {
renderData(data.elems, nesting, path, $group);
renderData($tpl, module, data.elems, nesting, path, $group);
}
$group.append(']');
break;
@ -268,11 +283,11 @@ function renderData(data, nesting, path, $group) {
case "map":
$group.append('{\n')
nesting++;
renderModuleInlineKey(data, nesting, $group);
renderModuleInlineKey($tpl, module, data, nesting, $group);
indent(nesting, $group);
renderData(data.map_keys, nesting, path, $group);
renderData($tpl, module, data.map_keys, nesting, path, $group);
$group.append(': ');
renderData(data.elems, nesting, path, $group);
renderData($tpl, module, data.elems, nesting, path, $group);
$group.append('\n');
nesting--;
indent(nesting, $group);
@ -282,18 +297,16 @@ function renderData(data, nesting, path, $group) {
case "module":
case "module_map":
var aTag = '<a';
if (canTraverse()) {
if (canTraverse(module)) {
aTag += ' href=".'+path+'/"';
}
aTag += ' class="module has-popup" data-namespace="'+(data.module_namespace || '')+'" data-path="'+path+'">&bull;&bull;&bull;</a>';
$group.append('{'+aTag+'}');
break;
}
$renderbox.append($group);
}
function renderModuleInlineKey(data, nesting, $group) {
function renderModuleInlineKey($tpl, module, data, nesting, $group) {
if (!data.module_inline_key) {
return
}
@ -305,15 +318,15 @@ function renderModuleInlineKey(data, nesting, $group) {
}
$group.append('\n');
appendToFieldDocs(data.module_inline_key, $('#hovercard-inline-key').html());
appendToFieldDocs($tpl, module, data.module_inline_key, $('#hovercard-inline-key').html());
}
function appendToFieldDocs(cleanFieldPath, fieldDoc) {
function appendToFieldDocs($tpl, module, cleanFieldPath, fieldDoc) {
var dt = cleanFieldPath;
if (canTraverse()) {
if (canTraverse(module)) {
dt = '<a href="./'+cleanFieldPath+'/">'+dt+'</a>';
}
$('#field-list-contents').append('<dt class="field-name" id="'+cleanFieldPath+'"><a href="#'+cleanFieldPath+'" class="inline-link">&#128279;</a>'+dt+'</dt> <dd>'+fieldDoc+'</dd>');
$('.field-list-contents', $tpl).append('<dt class="field-name" id="'+cleanFieldPath+'"><a href="#'+cleanFieldPath+'" class="inline-link">&#128279;</a>'+dt+'</dt> <dd>'+fieldDoc+'</dd>');
}
function indent(nesting, $group) {
@ -322,7 +335,7 @@ function indent(nesting, $group) {
$group.append($span);
}
function makeSubmoduleList(path, value) {
function makeSubmoduleList(module, path, value) {
while (value.elems) {
value = value.elems;
}
@ -330,10 +343,10 @@ function makeSubmoduleList(path, value) {
return '';
}
var submodList = '<ul>';
if (pageData.namespaces && pageData.namespaces[value.module_namespace]) {
for (var j = 0; j < pageData.namespaces[value.module_namespace].length; j++) {
var submod = pageData.namespaces[value.module_namespace][j];
var href = canTraverse() ? '.'+path+'/'+submod.name+'/' : './'+value.module_namespace+'.'+submod.name;
if (module.namespaces && module.namespaces[value.module_namespace]) {
for (var j = 0; j < module.namespaces[value.module_namespace].length; j++) {
var submod = module.namespaces[value.module_namespace][j];
var href = canTraverse(module) ? '.'+path+'/'+submod.name+'/' : './'+value.module_namespace+'.'+submod.name;
var submodLink = '<a href="'+href+'">'+submod.name+'</a>';
if (!isStandard(submod.package)) {
submodLink += ' '+nonStandardFlag;
@ -349,18 +362,22 @@ function makeSubmoduleList(path, value) {
// includes breadcrumbs; i.e. we are on a page
// that can traverse the JSON structure, not
// only render part of it in isolation.
function canTraverse() {
return pageData.breadcrumb != null;
function canTraverse(data) {
return data.breadcrumb != null;
}
function replaceGoTypeNameWithCaddyModuleName(docs, moduleID) {
function newGroup() {
return $('<div class="group"/>');
}
function replaceGoTypeNameWithCaddyModuleName(docs, module, moduleID) {
if (!docs || !moduleID) return docs;
// fully qualified type name
let fqtn = pageData.structure.type_name;
let fqtn = module.structure.type_name;
// extract just the local type name
let typeName = fqtn.substr(fqtn.lastIndexOf('.')+1)
let {_, typeName} = splitTypeName(fqtn);
// replace the type name with the Caddy module ID if it starts the docs.
if (docs.indexOf(typeName) === 0) {

View file

@ -80,3 +80,17 @@ $(function() {
});
}
});
function stripScheme(url) {
return url.substring(url.indexOf("://")+3);
}
// splitTypeName splits a fully qualified type name into
// its package path and type name components, for example:
// "github.com/foo/bar.Type" => "github.com/foo/bar" and "Type".
function splitTypeName(fqtn) {
let lastDotPos = fqtn.lastIndexOf('.');
let pkg = fqtn.substr(0, lastDotPos);
let typeName = fqtn.substr(lastDotPos+1);
return {pkg: pkg, typeName: typeName};
}

View file

@ -25,7 +25,7 @@ $.get("/api/packages").done(function(json) {
const moduleTemplate =
'<div class="module">\n'+
' &#128268;<a target="_blank" title="View module docs" class="module-link"></a>\n'+
' &#128268; <a target="_blank" title="View module docs" class="module-link"></a>\n'+
' <span class="module-desc"></span>\n'+
'</div>\n';
@ -53,6 +53,7 @@ $.get("/api/packages").done(function(json) {
for (var j = 0; j < pkg.modules.length; j++) {
var mod = pkg.modules[j];
var $mod = $(moduleTemplate);
// TODO: if this module name collides with that from another package, add a #hash to the URL to expand the right module's docs automatically
$('.module-link', $mod).attr('href', '/docs/modules/'+mod.name).text(mod.name).attr('title', "View module details");
$('.module-desc', $mod).text(moduleDocsPreview(mod, 120));
$('.package-modules', $pkg).append($mod);

View file

@ -9,7 +9,7 @@ setPageTitle();
$.get("/api/docs/config"+configPath, function(json) {
// wait until the DOM has finished loading before rendering the results
$(function() {
beginRendering(json.result);
beginRenderingInto($('#json-docs-container'), '', json.result);
// establish the breadcrumb
var $bc = $('.breadcrumbs');
@ -18,8 +18,8 @@ $.get("/api/docs/config"+configPath, function(json) {
var bcPath = pathComponents.slice(0, i+1).join('/');
var bcSiblingPath = pathComponents.slice(1, i).join('/');
// prefixing with <span/> is a hack so jQuery treats this as a HTML DOM object
$('<span/> &rsaquo; <a href="'+jsonDocsPathPrefix+bcPath.substr(1)+'/" class="breadcrumb has-popup" data-sibling-path="'+bcSiblingPath+'">'+pathComponents[i]+'</a>').appendTo($bc);
// enclosing with span is a hack so jQuery treats this as a HTML DOM object
$('<span> &rsaquo; <a href="'+jsonDocsPathPrefix+bcPath.substr(1)+'/" class="breadcrumb has-popup" data-sibling-path="'+bcSiblingPath+'">'+pathComponents[i]+'</a></span>').appendTo($bc);
}
});

View file

@ -2,14 +2,51 @@ const moduleDocsPathPrefix = "/docs/modules/";
var moduleID = window.location.pathname.substr(moduleDocsPathPrefix.length);
if (moduleID) {
// update page title and load the docs for this module
// update page title and load the docs for these modules (possibly more than 1 with this ID)
document.title = "Module " + moduleID + " - Caddy Documentation";
$.get("/api/docs/module/"+moduleID, function(json) {
var modules = json.result;
// wait until the DOM has finished loading before rendering the results
$(function() {
$('#module-docs-container').show();
$('h1').text("Module "+moduleID);
beginRendering(json.result, moduleID);
$('.module-name').text("Module "+moduleID);
modules.forEach((module) => {
$tpl = $('#module-template').clone().attr('id', stripScheme(module.repo));
if (modules.length > 1) {
$('article', $tpl).hide();
}
beginRenderingInto($tpl, moduleID, module);
$('#module-docs-container').append($tpl);
});
if (modules.length > 1) {
$('#module-multiple-repos .module-name').text(moduleID);
$('#module-multiple-repos').show();
} else {
$('.module-repo-selector').hide();
}
// if a specific repo's module is wanted, expand and scroll to it
if (window.location.hash.length > 1) {
// TODO: weird bug in jQuery(??) that it can't select IDs with slashes in them, so we use vanilla JS
var container = document.getElementById(window.location.hash.substr(1));
$('.module-repo-selector', container).click();
container.scrollIntoView();
}
});
});
$(function() {
$('body').on('click', '.module-repo-selector', function() {
if ($(this).hasClass('expanded')) {
// collapse
$('.module-repo-selector-arrow', this).html('&#9656;');
} else {
// expand
$('.module-repo-selector-arrow', this).html('&#9662;');
}
$(this).toggleClass('expanded');
$(this).next('article').toggle();
});
});
} else {
@ -17,24 +54,38 @@ if (moduleID) {
$.get("/api/modules", function(json) {
var moduleList = json.result;
console.log("MODULE LIST:", moduleList);
// wait until the DOM has finished loading before rendering the results
$(function() {
$('#module-list-container').show();
$table = $('#module-list');
for (modID in moduleList) {
var val = moduleList[modID];
var infos = moduleList[modID];
// refine a short preview of the module's docs
let shortDoc = truncate(val.doc, 200);
if (shortDoc && shortDoc.indexOf(modID) === 0) {
shortDoc = shortDoc.substr(modID.length).trim();
}
infos.forEach((info) => {
// refine a short preview of the module's docs
let shortDoc = truncate(info.docs, 200);
if (shortDoc && shortDoc.indexOf(modID) === 0) {
shortDoc = shortDoc.substr(modID.length).trim();
}
var standard = isStandard(val.type_name);
var $tr = $('<tr/>');
$tr.append('<td><a href="./'+modID+'" class="module-link">'+modID+'</a>'+(standard ? '' : ' '+nonStandardFlag)+'</td>');
$tr.append($('<td/>').text(shortDoc));
$table.append($tr);
let modLink = "./"+modID;
if (infos.length > 1) {
modLink += "#"+stripScheme(info.repo);
}
var standard = isStandard(info.package);
var $tr = $('<tr/>');
$tr.append('<td>'+(standard ? standardFlag : nonStandardFlag)+'</td>');
var $tdLink = $('<td><a href="'+modLink+'" class="module-link">'+modID+'</a></td>');
if (infos.length > 1) {
$tdLink.append($('<div class="module-repo-differentiator">').text('('+stripScheme(info.repo)+')'));
}
$tr.append($tdLink);
$tr.append($('<td/>').text(shortDoc));
$table.append($tr);
});
}
});
});