New download page, better JSON/module docs

This commit is contained in:
Matthew Holt 2021-05-25 17:26:35 -06:00
parent 09c3a73299
commit 5c3a67692e
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
8 changed files with 277 additions and 217 deletions

View file

@ -51,12 +51,18 @@
<option value="windows-amd64">Windows amd64</option>
<option value="windows-arm-6">Windows arm 6</option>
<option value="windows-arm-7">Windows arm 7</option>
<!-- <option value="windows-arm64">Windows arm64</option> TODO: Go 1.17 - https://github.com/golang/go/issues/36439 -->
</select>
</div>
</div>
<div>
<div>
<b>Additional packages:</b> <span id="package-count">0</span>
<b>Standard features:</b> &#9745;&#65039;
</div>
</div>
<div>
<div>
<b>Extra features:</b> <span id="package-count">0</span>
</div>
</div>
<div>
@ -64,24 +70,17 @@
</div>
</div>
<div class="packages-explanation">
Always comes with all standard <a href="/docs/modules">Caddy modules</a>.
<br><br>
Optionally select additional packages to include in your build: <span class="warning">⚠️ Only choose plugins you need and trust</span>
<input type="search" id="filter" placeholder="Filter packages and modules...">
<div class="text-center">
<span class="warning">⚠️ Only choose plugins you need and trust</span>
</div>
<div class="table-container">
<table id="optional-packages">
<tr>
<th>Package</th>
<th class="text-center">Version</th>
<th>Modules</th>
</tr>
<!-- Populated by JS -->
</table>
<div id="optional-packages">
<!-- Populated by JavaScript -->
</div>
</div>
</div>
{{include "/includes/footer.html"}}
</body>
</html>

View file

@ -720,7 +720,7 @@ td code {
#module-list td:first-child {
word-wrap: break-word;
max-width: 300px;
max-width: 400px;
}
#module-list .module-link {

View file

@ -28,27 +28,43 @@ body {
#download {
margin: 0;
font-size: 16px;
padding-left: 30px;
padding-right: 30px;
font-weight: bold;
}
.packages-explanation {
margin: 20px auto;
#filter {
margin-top: 1em;
width: 100%;
padding: 15px;
font-size: 24px;
text-align: center;
border: 0;
border-bottom: 1px solid #ccc;
outline: none;
}
#filter.found {
color: green;
}
#filter.not-found {
color: #cc0000;
}
.warning {
margin-left: 1em;
font-size: 12px;
margin-top: 20px;
display: inline-block;
font-size: 14px;
font-weight: bold;
padding: 2px 15px;
padding: 5px 15px;
border-radius: 1em;
border: 2px solid rgb(255, 201, 0);
color: rgb(206, 151, 0);
color: rgb(255, 208, 0);
background: #333;
}
input:disabled,
select:disabled,
#optional-packages.disabled .optpkg label {
#optional-packages.disabled {
cursor: not-allowed !important;
}
@ -129,133 +145,93 @@ select:disabled,
.table-container {
overflow-x: auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
#optional-packages {
width: 100%;
border-spacing: 0;
border-collapse: collapse;
margin-top: 2em;
}
/* #optional-packages tr {
height: 1px;
} */
#optional-packages th,
#optional-packages td {
border-bottom: 1px solid #dfeaf0;
}
#optional-packages th:first-child { border-top-left-radius: 8px; }
#optional-packages th:last-child { border-top-right-radius: 8px; }
#optional-packages th {
background: #dfe8ec;
text-align: left;
text-transform: uppercase;
letter-spacing: 2px;
color: #54676f;
font-weight: bold;
font-size: 12px;
margin-bottom: 1em;
border-color: #90a2ac;
}
#optional-packages th:first-child {
padding-left: 4.25em;
}
#optional-packages td {
background: #fff;
height: 1px; /* TODO: works on Chrome, but not Firefox */
/* height: 100%; TODO: works on Firefox, but not Chrome */
/* TODO:
see https://stackoverflow.com/a/34781198/1048862
(could also do tr with height: 1px which gets ignored,
then td with height: inherit; but this effectively the
same as just doing a td with height: 1px.)
I don't like how either Firefox or Chrome handle this
styling (setting the height of the parent shouldn't be
required at all, the browser is rendering a height
regardless!) but I think I lean toward Firefox's as
being more correct; the hack in Firefox is setting a
flexible 100% height on the parent, rather than the hack
in Chrome which is setting a height that is too small
and stretched or ignored anyway.
*/
}
#optional-packages th,
#optional-packages .optpkg td:first-child label,
#optional-packages td:not(:first-child) {
padding: 15px;
}
#optional-packages .optpkg-name {
font-weight: bold;
}
#optional-packages .optpkg input[type=checkbox] {
transform: scale(1.5);
cursor: pointer;
margin-right: 2em;
}
#optional-packages .optpkg label {
.package {
display: flex;
align-items: center;
height: 100%;
line-height: 1em;
padding: 2em;
background: rgba(255, 255, 255, .4);
border: 10px solid transparent;
box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
cursor: pointer;
transition: all 200ms ease-out;
}
#optional-packages:not(.disabled) .optpkg label:hover {
background: linear-gradient(90deg, rgba(242,248,253,1) 75%, rgba(242,248,253,0) 100%);
.package:hover {
transform: scale(1.02);
background: #fff;
box-shadow: 0 2px 20px -2px rgba(0, 0, 0, .05);
}
#optional-packages .optpkg.selected td {
background-color: #f2f8fd;
.package.selected {
border-color: rgb(25, 97, 192);
background: #fff;
}
#optional-packages .optpkg input[type=text] {
font-size: 12px;
padding: 6px;
outline: none;
.package-icon {
font-size: 48px;
margin-right: 20px;
}
.package-data {
flex-grow: 1;
}
.package-meta {
float: right;
font-size: 14px;
}
.package-downloads {
margin-right: 1em;
}
.package-version-input {
text-align: center;
border: 1px solid #ccc;
border-radius: 4px;
max-width: 75px;
padding: 4px;
margin-left: 5px;
border-radius: 5px;
border: 1px solid #aaa;
outline: none;
}
#optional-packages .optpkg input[type=text]::placeholder {
font-style: italic;
.package-link {
font-size: 20px;
font-weight: bold;
color: inherit;
word-break: break-all;
}
#optional-packages .optpkg-no-modules {
font-size: 12px;
.package-host {
font-size: 14px;
}
.package-modules {
margin-top: 1em;
}
.package-no-modules {
font-style: italic;
color: #555;
font-size: 14px;
}
#optional-packages .optpkg-module {
margin-top: .5em;
margin-bottom: .5em;
.module {
margin: 10px 0;
word-wrap: break-word;
}
#optional-packages .optpkg-module .module-name {
.module-link {
font-weight: bold;
font-family: 'PT Mono', monospace;
}
#optional-packages .optpkg-module .module-description {
color: #444;
margin-left: 1em;
.module-desc {
font-size: 14px;
margin-left: 1em;
}
@ -264,6 +240,9 @@ select:disabled,
.swal-custom-content {
text-align: left;
}

View file

@ -14,20 +14,23 @@ function isStandard(packagePath) {
return packagePath.startsWith(caddyImportPath);
}
function substrBeforeLastDot(s) {
return s.substr(0, s.lastIndexOf('.'))
}
function substrAfterLastDot(s) {
return s.substr(s.lastIndexOf('.'))
}
function truncate(str, len) {
function truncate(str, maxLen) {
if (!str) return "";
var startLen = str.length;
str = str.substring(0, len);
if (startLen > len) {
str += "...";
}
str = str.trim();
let firstPeriod = str.match(/\.(\s|$)/); // first dot not in the middle of a word, or at end of string
let terminate = firstPeriod ? firstPeriod.index+1 : str.length;
str = str.substring(0, terminate);
if (str.length <= maxLen) {
return str;
}
return str+"...";
}
function moduleDocsPreview(mod, maxLen) {
if (!mod || !mod.docs) return "";
let short = truncate(mod.docs, maxLen);
if (short.indexOf(mod.name) === 0) {
short = short.substr(mod.name.length).trim();
}
return short;
}

View file

@ -155,29 +155,23 @@ $(function() {
});
function beginRendering(json) {
function beginRendering(json, moduleID) {
pageData = json;
console.log("DATA:", pageData);
console.log("PAGE DATA:", pageData);
// show notice if module is non-standard
if (pageData.structure.type_name && !isStandard(pageData.structure.type_name)) {
var projectHref = 'https://'+pageData.structure.type_name;
projectHref = substrBeforeLastDot(projectHref);
$('.nonstandard-project-link').attr('href', projectHref).text(projectHref);
if (pageData.repo && !isStandard(pageData.structure.type_name)) {
$('.nonstandard-project-link').attr('href', pageData.repo).text(pageData.repo);
$('.nonstandard-notice').prepend(nonStandardFlag).show();
}
if (pageData.structure.doc) {
// for most types, just render their docs
$('#top-doc').html(markdown(pageData.structure.doc));
} else if (pageData.structure.elems) {
// for maps or arrays, fall through to the underlying type
$('#top-doc').html(markdown(pageData.structure.elems.doc));
}
// 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;
$('#top-doc').html(markdown(replaceGoTypeNameWithCaddyModuleName(rawDocs, moduleID)));
$('#top-doc').append(makeSubmoduleList("", pageData.structure));
renderData(pageData.structure, 0, "", $('<div class="group"/>'));
console.log("DOCS:", pageDocs);
if ($('#field-list-contents').text().trim()) {
$('#field-list-header').show();
@ -359,6 +353,23 @@ function canTraverse() {
return pageData.breadcrumb != null;
}
function replaceGoTypeNameWithCaddyModuleName(docs, moduleID) {
if (!docs || !moduleID) return docs;
// fully qualified type name
let fqtn = pageData.structure.type_name;
// extract just the local type name
let typeName = fqtn.substr(fqtn.lastIndexOf('.')+1)
// replace the type name with the Caddy module ID if it starts the docs.
if (docs.indexOf(typeName) === 0) {
docs = moduleID + docs.substr(typeName.length);
}
return docs;
}
function markdown(input) {
if (!input) {
return "";

View file

@ -1,72 +1,68 @@
// download package list as soon as possible
$.get("/api/packages").done(function(json) {
// sort package list by most popular, seems to make sense for convenience
var packageList = json.result;
const preselectedPackage = new URL(window.location.href).searchParams.getAll("package") ;
packageList.sort((a, b) => {
return b.downloads > a.downloads ? 1 : -1;
});
const preselectedPackage = new URL(window.location.href).searchParams.getAll("package");
// wait until the DOM has finished loading before rendering the results
$(function() {
const optpkgTemplate =
'<tr class="optpkg">'+
' <td><label><input type="checkbox" class="optpkg-check"><span class="optpkg-name"></span></label></td>'+
' <td class="text-center"><input type="text" name="version" placeholder="latest" title="Package version" size="5"></td>'+
' <td class="optpkg-modules"></td>'+
'</tr>';
const packageTemplate =
'<div class="package">\n'+
' <div class="package-icon">&#128230;</div>\n'+
' <div class="package-data">\n'+
' <div class="package-meta">\n'+
' <b>downloads:</b> <span class="package-downloads"></span>\n'+
' <b>version:</b> <input type="text" class="package-version-input" name="version" placeholder="latest" title="Any version string recognized by `go get` can be used">\n'+
' </div>\n'+
' <a target="_blank" title="View package repo" class="package-link"></a>\n'+
' <div class="package-modules"></div>\n'+
' </div>\n'+
'</div>\n'
const moduleTemplate =
'<div class="module">\n'+
' &#128268;<a target="_blank" title="View module docs" class="module-link"></a>\n'+
' <span class="module-desc"></span>\n'+
'</div>\n';
const optpkgModuleTemplate =
'<div class="optpkg-module">'+
' <a class="module-name" title="View docs"></a>'+
' <span class="module-description"></span>'+
'</div>';
for (var i = 0; i < packageList.length; i++) {
var pkg = packageList[i];
if (isStandard(pkg.path)) {
// not necessary to show, since these packages
// come with standard distribution
continue;
}
var $optpkg = $(optpkgTemplate);
$('.optpkg-name', $optpkg).text(pkg.path);
var $pkg = $(packageTemplate);
let { provider, path } = splitVCSProvider(pkg.path);
if (provider) {
var $pkgHost = $('<span class="package-host"/>').text(provider);
$('.package-link', $pkg).html($pkgHost).append('<br/>');
}
$pkgName = $('<span class="package-name"/>').text(path);
$('.package-link', $pkg).append($pkgName);
$('.package-link', $pkg).prop('href', pkg.repo);
$('.package-downloads', $pkg).text(pkg.downloads);
if (preselectedPackage.includes(pkg.path)) {
$('.optpkg-check', $optpkg).prop("checked", true);
$('.optpkg-check', $optpkg).closest('.optpkg').toggleClass("selected");
$($pkg).addClass("selected");
}
$('.optpkg-check', $optpkg).change({pkg: pkg}, (event) => {
const element = $(event.currentTarget);
let newUrl = new URL(window.location.href);
let currentSelected = newUrl.searchParams.getAll("package") ;
newUrl.searchParams.delete("package");
const pkgPath = event.data.pkg.path;
if (element.is(':checked')) {
if (!currentSelected.includes(pkgPath)) {
currentSelected = [...currentSelected, pkgPath];
}
} else {
const position = currentSelected.indexOf(pkgPath);
if (position >= 0) {
currentSelected.splice(position, 1);
}
}
currentSelected.forEach( (selected) => newUrl.searchParams.append("package", selected));
history.replaceState({}, "Download Caddy", newUrl.toString());
});
if (pkg.modules && pkg.modules.length > 0) {
for (var j = 0; j < pkg.modules.length; j++) {
var mod = pkg.modules[j];
var $mod = $(optpkgModuleTemplate);
$('.module-name', $mod).attr('href', '/docs/modules/'+mod.name).text(mod.name);
$('.module-description', $mod).text(truncate(mod.docs, 120));
$('.optpkg-modules', $optpkg).append($mod);
var $mod = $(moduleTemplate);
$('.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);
}
} else {
$('.optpkg-modules', $optpkg)
.addClass("optpkg-no-modules")
.text('This package does not add any modules. Either it is another kind of plugin (such as a config adapter) or this listing is in error.');
$('.package-modules', $pkg)
.addClass("package-no-modules")
.text('This package does not add any modules to the JSON config structure. Either it is another kind of plugin (such as a config adapter) or this listing is in error.');
}
$('#optional-packages').append($optpkg);
$('#optional-packages').append($pkg);
}
updatePage();
});
@ -86,19 +82,71 @@ $(function() {
downloadButtonHtml = $('#download').html();
// update the page, including the download link, when form fields change
$('#optional-packages').on('change', 'input[type=checkbox]', function() {
$(this).closest('.optpkg').toggleClass('selected');
updatePage();
$('#filter').on('search keyup', function(event) {
var count = 0;
var q = $(this).val().trim().toLowerCase();
$('.package').each(function() {
if (!q) {
// filter is cleared; show all
this.style.display = '';
return;
}
var corpus = $(this).find('.package-link, .module-link, .module-desc').text().trim().toLowerCase();
if (corpus.indexOf(q) === -1) {
this.style.display = 'none';
return;
}
this.style.display = '';
count++;
});
$('#optional-packages').on('change keyup', 'input[name=version]', function() {
updatePage();
// update color of search input based on results
if (q) {
if (count > 0) {
$('#filter').addClass('found').removeClass('not-found');
} else {
$('#filter').addClass('not-found').removeClass('found');
}
} else {
$('#filter').removeClass('found not-found');
}
});
$('#platform').change(function() {
updatePage();
});
$('#download').click(function(event) {
$('#optional-packages').on('click', '.package', function() {
$(this).toggleClass('selected');
updatePage();
let newUrl = new URL(window.location.href);
let currentSelected = newUrl.searchParams.getAll("package") ;
newUrl.searchParams.delete("package");
const pkgPath = $('.package-link', $(this)).text().trim();
if ($(this).hasClass('selected')) {
if (!currentSelected.includes(pkgPath)) {
currentSelected = [...currentSelected, pkgPath];
}
} else {
const position = currentSelected.indexOf(pkgPath);
if (position >= 0) {
currentSelected.splice(position, 1);
}
}
currentSelected.forEach( (selected) => newUrl.searchParams.append("package", selected));
history.replaceState({}, document.title, newUrl.toString());
});
// when a link within a package listing is clicked, only operate the link (don't select the package)
$('#optional-packages').on('click', '.package-link, .module-link, .package-version-input', function(event) {
event.stopPropagation();
});
$('#download').click(function() {
if ($(this).hasClass('disabled')) {
return false;
}
@ -180,7 +228,7 @@ function getDownloadLink() {
// get plugins and their versions
$('#optional-packages .selected').each(function() {
// get package path
var p = $('.optpkg-name', this).text().trim();
var p = $('.package-link', this).text().trim();
// get package version, if user specified one
var ver = $('input[name=version]', this).val().trim();
@ -216,7 +264,7 @@ function handleBuildError(jqxhr, status, error) {
}
function updatePage() {
$('#package-count').text($('.optpkg.selected').length);
$('#package-count').text($('.package.selected').length);
$('#download').attr('href', getDownloadLink());
}
@ -244,4 +292,17 @@ function enableFields() {
window.onbeforeunload = null;
}
function splitVCSProvider(pkgPath) {
var providers = ["github.com/", "bitbucket.org/"];
for (var i = 0; i < providers.length; i++) {
if (pkgPath.toLowerCase().indexOf(providers[i]) == 0) {
return {
provider: providers[i],
path: pkgPath.slice(providers[i].length)
};
}
}
return {provider: "", path: pkgPath};
}
var downloadButtonHtml; // to restore button to its original contents

View file

@ -9,7 +9,7 @@ if (moduleID) {
$(function() {
$('#module-docs-container').show();
$('h1').text("Module "+moduleID);
beginRendering(json.result);
beginRendering(json.result, moduleID);
});
});
} else {
@ -23,10 +23,17 @@ if (moduleID) {
$table = $('#module-list');
for (modID in moduleList) {
var val = 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/>');
$tr.append('<td><a href="./'+modID+'" class="module-link">'+modID+'</a>'+(standard ? '' : ' '+nonStandardFlag)+'</td>');
$tr.append('<td>'+markdown(truncate(val.doc, 200))+'</td>');
$tr.append($('<td/>').text(shortDoc));
$table.append($tr);
}
});