From e132c5c45422c92fd8b9d385e3e2c01e25fa1857 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 2 Jun 2021 15:16:15 -0600 Subject: [PATCH] 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. --- src/docs/index.html | 6 +- src/docs/json/index.html | 16 ++-- src/docs/modules/index.html | 53 +++++++----- src/download.html | 2 +- src/includes/docs-details.html | 10 ++- src/includes/docs-renderbox.html | 2 +- src/resources/css/docs-json.css | 2 +- src/resources/css/docs.css | 49 ++++++++++- src/resources/js/docs-api.js | 143 +++++++++++++++++-------------- src/resources/js/docs.js | 14 +++ src/resources/js/download.js | 3 +- src/resources/js/json-docs.js | 8 +- src/resources/js/module-docs.js | 83 ++++++++++++++---- 13 files changed, 265 insertions(+), 126 deletions(-) diff --git a/src/docs/index.html b/src/docs/index.html index d474bac..e233c24 100644 --- a/src/docs/index.html +++ b/src/docs/index.html @@ -18,9 +18,9 @@
-
- {{markdown $markdownFile.Body}} -
+
+
{{markdown $markdownFile.Body}}
+
diff --git a/src/docs/json/index.html b/src/docs/json/index.html index a16c1bc..2ff5090 100644 --- a/src/docs/json/index.html +++ b/src/docs/json/index.html @@ -15,13 +15,15 @@
-
- - {{include "/includes/docs-renderbox.html"}} - {{include "/includes/docs-details.html"}} -
+
+
+ + {{include "/includes/docs-renderbox.html"}} + {{include "/includes/docs-details.html"}} +
+
diff --git a/src/docs/modules/index.html b/src/docs/modules/index.html index b60d7db..786feab 100644 --- a/src/docs/modules/index.html +++ b/src/docs/modules/index.html @@ -15,27 +15,38 @@
-
-

All Modules

-

- This page lists all registered Caddy modules. -

-

- We recommend using your browser's "Find in page" feature for quick lookups. -

- - - - - - -
Module IDDescription
-
-
-

- {{include "/includes/docs-renderbox.html"}} - {{include "/includes/docs-details.html"}} -
+
+
+

All Modules

+

+ This page lists all registered Caddy modules. Modules are plugins which extend Caddy's JSON configuration structure. +

+

+ We recommend using your browser's "Find in page" feature for quick lookups. +

+ + + + + + + +
Module IDDescription
+
+
+

+
+ There is more than one module named . Choose one by its repository. +
+
+
+
+ {{include "/includes/docs-renderbox.html"}} + {{include "/includes/docs-details.html"}} +
+
+
+
diff --git a/src/download.html b/src/download.html index 164a3c0..afa53f6 100644 --- a/src/download.html +++ b/src/download.html @@ -57,7 +57,7 @@
- Standard features: ☑️ + Standard features: ☑️
diff --git a/src/includes/docs-details.html b/src/includes/docs-details.html index 5148da2..9b3b240 100644 --- a/src/includes/docs-details.html +++ b/src/includes/docs-details.html @@ -2,15 +2,17 @@
This module does not come with Caddy. It can be added by using xcaddy or our download page. 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.

- Module repository: + Code repository: +

+ Custom builds: xcaddy build --with

Description

-
+
-

Field List

-
+

Field List

+
\ No newline at end of file diff --git a/src/includes/docs-renderbox.html b/src/includes/docs-renderbox.html index a778960..ea506df 100644 --- a/src/includes/docs-renderbox.html +++ b/src/includes/docs-renderbox.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/src/resources/css/docs-json.css b/src/resources/css/docs-json.css index ae0da31..cb31773 100644 --- a/src/resources/css/docs-json.css +++ b/src/resources/css/docs-json.css @@ -6,7 +6,7 @@ article h1 { padding-top: 8%; } -#renderbox { +.renderbox { border-radius: 0; font-size: 20px; line-height: 1.6em; diff --git a/src/resources/css/docs.css b/src/resources/css/docs.css index 98895f4..5dadcea 100644 --- a/src/resources/css/docs.css +++ b/src/resources/css/docs.css @@ -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; +} diff --git a/src/resources/js/docs-api.js b/src/resources/js/docs-api.js index 9cef567..1e52180 100644 --- a/src/resources/js/docs-api.js +++ b/src/resources/js/docs-api.js @@ -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 = 'Non-standard'; +const standardFlag = 'Standard'; + $(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('▸'); @@ -37,7 +38,9 @@ $(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, // then we should ignore the hover on this link to allow cursor to visit @@ -59,10 +62,10 @@ $(function() { if ($(this).hasClass('module')) { // module var $list =$('
'); - 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 = ' '+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 beginRenderingInto($tpl, moduleID, module) { + console.log("RENDERING:", moduleID, module); + $tpl.data('module-id', moduleID); + pageData[moduleID] = module; -function beginRendering(json, moduleID) { - pageData = json; - console.log("PAGE DATA:", pageData); - // 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 = $('').text(stripScheme(module.repo)); + $('.module-repo-selector', $tpl).html('').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, "", $('
')); + 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('{'); nesting++; - var $fieldGroup = $('
'); - 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) || '

There are no docs for this property.

'; - 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 = $('
'); + var $fieldGroup = newGroup(); indent(nesting, $fieldGroup); var keyATag = ''+field.key+''; $fieldGroup.append('"'+keyATag+'": '); - 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('{•••}'); } 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 = '•••'; $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 = ''+dt+''; } - $('#field-list-contents').append('
🔗'+dt+'
'+fieldDoc+'
'); + $('.field-list-contents', $tpl).append('
🔗'+dt+'
'+fieldDoc+'
'); } 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 = '
    '; - 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 = ''+submod.name+''; 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 $('
    '); +} + +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) { diff --git a/src/resources/js/docs.js b/src/resources/js/docs.js index de71c2f..5658240 100644 --- a/src/resources/js/docs.js +++ b/src/resources/js/docs.js @@ -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}; +} \ No newline at end of file diff --git a/src/resources/js/download.js b/src/resources/js/download.js index 27e7651..d7673cd 100644 --- a/src/resources/js/download.js +++ b/src/resources/js/download.js @@ -25,7 +25,7 @@ $.get("/api/packages").done(function(json) { const moduleTemplate = '
    \n'+ - ' 🔌\n'+ + ' 🔌 \n'+ ' \n'+ '
    \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); diff --git a/src/resources/js/json-docs.js b/src/resources/js/json-docs.js index e365cad..2967be1 100644 --- a/src/resources/js/json-docs.js +++ b/src/resources/js/json-docs.js @@ -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'); @@ -17,9 +17,9 @@ $.get("/api/docs/config"+configPath, function(json) { for (var i = 1; i < pathComponents.length-1; i++) { var bcPath = pathComponents.slice(0, i+1).join('/'); var bcSiblingPath = pathComponents.slice(1, i).join('/'); - - // prefixing with is a hack so jQuery treats this as a HTML DOM object - $(''+pathComponents[i]+'').appendTo($bc); + + // enclosing with span is a hack so jQuery treats this as a HTML DOM object + $(''+pathComponents[i]+'').appendTo($bc); } }); diff --git a/src/resources/js/module-docs.js b/src/resources/js/module-docs.js index 12bd5a0..6e0d448 100644 --- a/src/resources/js/module-docs.js +++ b/src/resources/js/module-docs.js @@ -2,40 +2,91 @@ 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('▸'); + } else { + // expand + $('.module-repo-selector-arrow', this).html('▾'); + } + $(this).toggleClass('expanded'); + $(this).next('article').toggle(); }); }); } else { // populate the module list $.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(); - } - - var standard = isStandard(val.type_name); - var $tr = $(''); - $tr.append(''+modID+''+(standard ? '' : ' '+nonStandardFlag)+''); - $tr.append($('').text(shortDoc)); - $table.append($tr); + 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(); + } + + let modLink = "./"+modID; + if (infos.length > 1) { + modLink += "#"+stripScheme(info.repo); + } + + var standard = isStandard(info.package); + var $tr = $(''); + $tr.append(''+(standard ? standardFlag : nonStandardFlag)+''); + var $tdLink = $(''+modID+''); + if (infos.length > 1) { + $tdLink.append($('
    ').text('('+stripScheme(info.repo)+')')); + } + $tr.append($tdLink); + $tr.append($('').text(shortDoc)); + $table.append($tr); + }); } }); }); -} \ No newline at end of file +}