New Website: Phase I (#357)

* Initial commit; starting new design

Dropdown menu

* Begin docs layout of new design

* Get themes under control; button hover splash

* Some basic responsiveness

* Finish responsive layout; several bug fixes

* Avoid flash during color scheme change

* Begin building top of homepage

* docs: Start building quick-assist feature

* Work on homepage a little more

* Keep working on homepage

* More homepage progress

* Some sponsor SVGs

* Add sponsor features

* Implement basic Sponsor Experience box

* Reorganize some styles

* WIP sponsors page

* Start features page WIP

* Minor improvements

* Fix headings; work on features page

* WIP features page

* Continue work on marketing pages

* Continue work on features page

* More features WIP

* Continue features page...

* More work on features page

* Keeping going  :)

* Continue home and features pages

* More homepage/features content, screenshots, tweaks

* Minor fixes to features page

* Minor tweaks

* Work on testimonials

* Work on homepage more

* More homepage work

* Continue work on homepage

* Add some sponsor logos

* Some citation screenshots

* Add citations

* Start making homepage responsive

* Re-add cache busting

Fix docs

* Use markdown syntax highlighting on frontpage

* Rework AJQuery to $_ to not interfere with jQuery

* Rewrite quick assist with AlpineJS, use markdown for contents

* More work on marketing pages

* Rebase and fix code displays

* Syntax highlight on-demand example, fix rollover

* Adjust on-demand demo

* Work on responsiveness

* Keep working on responsiveness

* Mainly finish making design responsive

* Thiccer favicon

* More work on marketing pages

* Keep on going

* Fix link

* Move new site into src folder

* Add open graph image

* Add recorded demo for homepage

* Tweak caption

* Fix Poppins font for now

* Minor tweaks

* Trim demo ending

* Remove unfinished pages

Also update Framer logo

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
This commit is contained in:
Matt Holt 2023-12-11 10:07:34 -07:00 committed by GitHub
parent 5bb6d92c63
commit 07c51663ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
191 changed files with 13008 additions and 4970 deletions

View file

@ -0,0 +1,63 @@
if (!loggedIn()
&& window.location.pathname != '/account/login'
&& window.location.pathname != '/account/create'
&& window.location.pathname != '/account/verify'
&& window.location.pathname != '/account/reset-password') {
window.location = '/account/login?redir='+encodeURIComponent(window.location);
}
$(function() {
// highlight current page in left nav
var $currentPageLink = $('.container > nav a[href="'+window.location.pathname+'"]');
$currentPageLink.addClass('current');
// shortcut any logout links to make the POST directly
$('a[href="/account/logout"]').click(function() {
logout();
return false;
});
});
function loggedIn() {
return document.cookie.indexOf('user=') > -1;
}
function logout() {
$.post('/api/logout').done(function() {
window.location = '/';
}).fail(function(jqxhr, status, error) {
document.cookie = 'user=; Path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
swal({
icon: "error",
title: error,
content: errorContent(jqxhr)
}).then(function() {
window.location = '/account/'
});
});
}
function errorContent(jqxhr) {
var div = document.createElement('div');
var p1 = document.createElement('p');
p1.appendChild(document.createTextNode("Sorry, something went wrong:"));
div.appendChild(p1);
var p2 = document.createElement('p');
var p2b = document.createElement('b');
p2b.appendChild(document.createTextNode(jqxhr.responseJSON ? jqxhr.responseJSON.error.message : jqxhr.status + " " + jqxhr.statusText));
p2.appendChild(p2b)
div.appendChild(p2);
if (jqxhr.responseJSON) {
var p3 = document.createElement('p');
p3.appendChild(document.createTextNode("Please include this error ID if reporting:"));
p3.appendChild(document.createElement('br'));
p3.appendChild(document.createTextNode(jqxhr.responseJSON.error.id));
div.appendChild(p3);
}
return div;
}

View file

@ -0,0 +1,28 @@
if (loggedIn()) window.location = '/account/';
$(function() {
$('form input').first().focus();
$('form').submit(function(event) {
$('#submit').prop('disabled', true);
$.post($(this).prop("action"), $(this).serialize()).done(function() {
swal({
icon: "success",
title: "Check your email",
text: "We've sent you an email with a link that expires in 48 hours. Please verify your account before you can use it."
}).then(function() {
window.location = '/account/verify';
});
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
titleText: "Error",
content: errorContent(jqxhr)
});
$('#submit').prop('disabled', false);
});
return false;
});
});

View file

@ -0,0 +1,128 @@
// download package list as soon as possible
$.get("/api/user-packages").done(function(json) {
var packageList = json.result;
// wait until the DOM has finished loading before rendering the results
$(function() {
// trying out this fancy new syntax:
// https://twitter.com/joshmanders/status/1282395540970496001
packageList.forEach(pkg => {
var $tdPath = $('<td><input type="text" name="path" maxlength="255"></td>');
var $tdListed = $('<td class="text-center"><input type="checkbox" name="listed"></td>');
var $tdAvail = $('<td class="text-center"><input type="checkbox" name="available"></td>');
var $tdDownloads = $('<td>0</td>');
var $tdLinks = $('<td><a href="javascript:" class="rescan-package">Rescan</a> &nbsp; <a href="javascript:" class="delete-package">Delete</a></td>');
if (pkg.listed) {
$('input', $tdListed).prop('checked', true);
}
if (pkg.available) {
$('input', $tdAvail).prop('checked', true);
}
if (pkg.downloads) {
$tdDownloads.text(pkg.downloads);
}
var $pathInput = $('input', $tdPath);
$pathInput.val(pkg.path).attr('size', pkg.path.length);
var $tr = $('<tr data-package-id="'+pkg.id+'"></tr>');
$tr.append($tdPath)
.append($tdListed)
.append($tdAvail)
.append($tdDownloads)
.append($tdLinks);
$('#user-packages').append($tr);
// scroll package paths to the left so if they get
// cut off, the leaf package name is still visible
$pathInput.scrollLeft($pathInput.width());
});
});
});
$(function() {
// update packages when fields change
$('#user-packages').on('change', 'input', function() {
$tr = $(this).closest('tr');
$('input', $tr).prop('disabled', true);
$.post('/api/update-package', {
id: $tr.data('package-id'),
listed: $('[name=listed]', $tr).prop('checked') ? 1 : 0,
available: $('[name=available]', $tr).prop('checked') ? 1 : 0,
path: $('[name=path]', $tr).val()
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
title: "Could not save changes",
content: errorContent(jqxhr)
});
}).always(function() {
$('input', $tr).prop('disabled', false);
});
});
// rescan package
$('#user-packages').on('click', '.rescan-package', function() {
if ($(this).hasClass('disabled')) return;
$tr = $(this).closest('tr');
$('a', $tr).addClass('disabled');
$.post('/api/rescan-package', {
package_id: $tr.data('package-id')
}).done(function(jqxhr, status, error) {
swal({
icon: "success",
title: "Rescan Complete",
text: "Package has been re-scanned and its documentation has been updated."
});
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
title: "Rescan failed",
content: errorContent(jqxhr)
});
}).always(function() {
$('a', $tr).removeClass('disabled');
});
});
// delete package
$('#user-packages').on('click', '.delete-package', function() {
if ($(this).hasClass('disabled')) return;
swal({
title: "Delete package?",
text: "Deleting the package will remove it from our website.",
icon: "warning",
buttons: true,
dangerMode: true,
}).then((willDelete) => {
// abort if user cancelled
if (!willDelete) return;
$tr = $(this).closest('tr');
$('input', $tr).prop('disabled', true);
$.post('/api/delete-package', {
id: $tr.data('package-id')
}).done(function(jqxhr, status, error) {
$tr.remove();
swal({
icon: "success",
title: "Package deleted"
});
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
title: "Delete failed",
content: errorContent(jqxhr)
});
}).always(function() {
$('input', $tr).prop('disabled', false);
});
});
});
});

View file

@ -0,0 +1,24 @@
if (loggedIn()) window.location = '/account/';
$(function() {
$('form input').first().focus();
$('form').submit(function(event) {
$('#submit').prop('disabled', true);
$.post($(this).prop("action"), $(this).serialize()).done(function() {
var qsParams = new URLSearchParams(window.location.search);
var destination = qsParams.get('redir');
window.location = destination ? destination : '/account/';
}).fail(function(jqxhr, msg, error) {
swal({
icon: "error",
title: "Bad credentials",
content: errorContent(jqxhr)
});
$('#submit').prop('disabled', false);
});
return false;
});
});

View file

@ -0,0 +1 @@
logout();

View file

@ -0,0 +1,27 @@
$(function() {
$('form input').first().focus();
$('form').submit(function(event) {
$('#submit').prop('disabled', true);
$.post($(this).prop("action"), $(this).serialize()).done(function() {
swal({
icon: "success",
title: "It's yours",
text: "Package claimed. Its documentation is now available on our website and you are responsible for maintaining it. Thank you!"
}).then(function() {
// TODO: ...
// window.location = "/account/login";
});
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
title: error,
content: errorContent(jqxhr)
});
$('#submit').prop('disabled', false);
});
return false;
});
});

View file

@ -0,0 +1,80 @@
if (loggedIn()) window.location = '/account/';
$(function() {
var qsParams = new URLSearchParams(window.location.search);
var email = qsParams.get("email");
var token = qsParams.get("token");
$('input[name=email]').val(email);
$('input[name=token]').val(token);
if (email && token) showStep2();
$('form input:visible').first().focus();
$('#reset-password-step1').submit(function(event) {
$('button').prop('disabled', false);
$.post($(this).prop("action"), $(this).serialize()).done(function() {
swal({
icon: "info",
title: "Check your email",
text: "If we have an account with that email address, we just sent you some instructions."
}).then(function() {
window.location = '/';
});
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
title: error,
content: errorContent(jqxhr)
});
$('button').prop('disabled', false);
});
return false;
});
$('#reset-password-step2').submit(function(event) {
$('button').prop('disabled', false);
$.post($(this).prop("action"), $(this).serialize()).done(function() {
swal({
icon: "success",
title: "Reset completed",
text: "You may now log in with your new password."
}).then(function() {
window.location = '/account/login';
});
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
title: "Error",
content: errorContent(jqxhr)
});
$('button').prop('disabled', false);
});
return false;
});
$('#goto-step1').click(function(event) {
$('#reset-password-step2').hide('fast');
$('#reset-password-step1').show('fast', function() {
$('input:visible').first().focus();
});
return false;
});
$('#goto-step2').click(function(event) {
showStep2();
return false;
});
});
function showStep2() {
$('#reset-password-step1').hide('fast');
$('#reset-password-step2').show('fast', function() {
if ($('input[name=token]').val() != "")
$('input[name=password]').focus();
else
$('input:visible').first().focus();
});
}

View file

@ -0,0 +1,36 @@
if (loggedIn()) window.location = '/account/';
$(function() {
$('form input').first().focus();
$('form').submit(function(event) {
$('#submit').prop('disabled', true);
$.post($(this).prop("action"), $(this).serialize()).done(function() {
swal({
icon: "success",
title: "Account confirmed",
text: "Thank you. You may now log in and use your account!"
}).then(function() {
window.location = "/account/login";
});
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
title: error,
content: errorContent(jqxhr)
});
$('#submit').prop('disabled', false);
});
return false;
});
// if info is in the query string, fill use and submit it
var qsParams = new URLSearchParams(window.location.search);
var email = qsParams.get("email");
var acct = qsParams.get("code");
$('input[name=email]').val(email);
$('input[name=account_id]').val(acct);
if (email && acct) $('form').submit();
});

View file

@ -0,0 +1,81 @@
document.addEventListener('DOMContentLoaded', function() {
// Algolia search
docsearch({
appId: "BH4D9OD16A",
apiKey: '14275a785f6ebd31d162f9d2d8fc0125',
indexName: 'caddyserver',
container: '#search',
});
});
const caddyImportPath = 'github.com/caddyserver/caddy/v2';
function isStandard(packagePath) {
return packagePath.startsWith(caddyImportPath);
}
function truncate(str, maxLen) {
if (!str) return "";
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;
}
function detectPlatform() {
// assume 32-bit linux, then change OS and architecture if justified
var os = "linux", arch = "amd64";
// change os
if (/Macintosh/i.test(navigator.userAgent)) {
os = "darwin";
} else if (/Windows/i.test(navigator.userAgent)) {
os = "windows";
} else if (/FreeBSD/i.test(navigator.userAgent)) {
os = "freebsd";
} else if (/OpenBSD/i.test(navigator.userAgent)) {
os = "openbsd";
}
// change architecture
if (os == "darwin" || /amd64|x64|x86_64|Win64|WOW64|i686|64-bit/i.test(navigator.userAgent)) {
arch = "amd64";
} else if (/arm64/.test(navigator.userAgent)) {
arch = "arm64";
} else if (/ ARM| armv/.test(navigator.userAgent)) {
arch = "arm";
}
// change arm version
if (arch == "arm") {
var arm = "7"; // assume version 7 by default
if (/armv6/.test(navigator.userAgent)) {
arm = "6";
} else if (/armv5/.test(navigator.userAgent)) {
arm = "5";
}
arch += arm;
}
return [os, arch];
}
// Detect the platform OS, but with an allow-list of values
// and if the value is not allowed, return the default.
function defaultOS(allowed, def) {
var [os] = detectPlatform();
return allowed.includes(os) ? os : def;
}

View file

@ -0,0 +1,393 @@
// TODO: sanitize all HTML renderings, especially markdown: https://github.com/cure53/DOMPurify
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() {
$hovercard = $('#hovercard');
var hoverTimeout;
$hovercard.hover(function() {
clearTimeout(hoverTimeout);
}, function() {
clearTimeout(hoverTimeout);
$hovercard.removeClass('popup');
});
// toggle an object as expanded or collapsed
$('body').on('click', '.renderbox .toggle-obj', function() {
if ($(this).hasClass('expanded')) {
// collapse
$(this).html('&#9656;');
} else {
// expand
$(this).html('&#9662;');
}
$(this).nextUntil('.end-obj').toggleClass('collapsed');
$(this).toggleClass('expanded');
});
$('body').on({
mouseenter: function() {
// don't allow hoverbox to close anymore, we're re-opening it
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
// the hoverbox if that is where it is going; this makes it possible to
// visit the hoverbox while it is over a list of links that are tightly
// stacked vertically; if user wants to visit hoverbox for link in this
// gap, they can just move the cursor slow enough to fire the timeout
if ($hovercard.is(':visible') && $hovercard.offset().top - 10 < pos.top) {
return;
}
// fill out hovercard
var elemPath = $(this).data('path');
var modNamespace = $(this).data('namespace');
$('.hovercard-elem').hide();
if ($(this).hasClass('module')) {
// module
var $list =$('<div/>');
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;
}
content += '<span class="module-link-description">'+truncate(modInfo.docs, 115)+'</span></a>';
$list.append(content);
}
}
$('#hovercard-module-list').html($list);
$('#hovercard-namespace').text(modNamespace)
$('#hovercard-module').show();
} else if ($(this).hasClass('module-inline-key')) {
// inline key
$('#hovercard-inline-key').show();
} else if ($(this).hasClass('breadcrumb')) {
// breadcrumb siblings
var siblingPath = $(this).data('sibling-path');
var bcVal = moduleData.breadcrumb[siblingPath];
var bcSiblings = [];
// drill down to the true underlying type
while (bcVal.elems) {
bcVal = bcVal.elems;
}
switch (bcVal.type) {
case "struct":
for (var j = 0; j < bcVal.struct_fields.length; j++) {
var sf = bcVal.struct_fields[j];
bcSiblings.push({name: sf.key, path: siblingPath, isStandard: isStandard(bcVal.type_name)})
}
break;
case "module":
case "module_map":
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)})
}
}
var $siblings = $('<div class="breadcrumb-siblings"/>').append('<div class="breadcrumb-siblings-title">Siblings:</div>');
for (var j = 0; j < bcSiblings.length; j++) {
var sib = bcSiblings[j];
var sibPath = sib.path;
if (sibPath) {
sibPath += "/";
}
sibPath += sib.name+"/";
var aTag = '<a href="'+jsonDocsPathPrefix+sibPath+'"';
if (!sib.isStandard) {
aTag += ' class="nonstandard" title="Non-standard module"';
}
aTag += '>'+sib.name+'</a>';
$siblings.append(aTag);
}
$('#hovercard-breadcrumb-siblings').html($siblings).show();
} else if ($(this).hasClass('documented')) {
// docs
var elemDocs = truncate(pageDocs[elemPath], 500);
if (!elemDocs) {
elemDocs = '<p class="explain">There are no docs for this property.</p>';
return;
}
$('#hovercard-docs').html(markdown(elemDocs)).show();
$('#hovercard-inline-link').html('<a href="#'+elemPath.substr(1)+'">View docs below &#8595;</a>').show();
}
// show hoverbox for this link
var height = $(this).height();
var linkWidth = $(this).width();
var boxWidth = $hovercard.width();
$hovercard.css({
'top': pos.top + height*1.5 + 10, // '+10' counters 'translateY(-10px)'
'left': pos.left + (linkWidth/2) - (boxWidth/2)
}).addClass('popup');
},
mouseleave: function() {
// don't hide the hoverbox right away; user might need a
// few milliseconds to get the cursor over the hovercard
hoverTimeout = setTimeout(function() {
$hovercard.removeClass('popup');
}, 200);
}
}, '.has-popup');
});
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 (module.repo) {
if (!isStandard(module.structure.type_name)) {
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).css('display', 'block').prepend(nonStandardFlag); // for some reason show() dosen't work
}
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 = module.structure.doc ?? module.structure.elems;
$('.top-doc', $tpl).html(markdown(replaceGoTypeNameWithCaddyModuleName(rawDocs, module, moduleID)));
$('.top-doc', $tpl).append(makeSubmoduleList(module, "", module.structure));
let $group = newGroup();
renderData($tpl, module, module.structure, 0, "", $group);
$('.renderbox', $tpl).append($group);
if ($('.field-list-contents', $tpl).text().trim()) {
$('.field-list-header', $tpl).show();
}
// 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($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 = 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...
// data.struct_fields.sort(function(a, b) {
// if (a.key > b.key) return 1;
// if (b.key > a.key) return -1;
// return 0;
// });
for (var i = 0; i < data.struct_fields.length; i++) {
var field = data.struct_fields[i];
var fieldPath = path+"/"+field.key;
var cleanFieldPath = fieldPath.slice(1); // trim leading slash
// store the docs for this path
let linkClass = "documented";
if (field.doc) {
pageDocs[fieldPath] = field.doc;
linkClass += " has-popup";
}
// render the docs to the page
var fieldDoc = markdown(field.doc) || '<p class="explain">There are no docs for this property.</p>';
fieldDoc += makeSubmoduleList(module, fieldPath, field.value);
appendToFieldDocs($tpl, module, cleanFieldPath, fieldDoc);
// render the field to the JSON box
var $fieldGroup = newGroup();
indent(nesting, $fieldGroup);
var keyATag = '<a ';
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($tpl, module, field.value, nesting, fieldPath, $fieldGroup);
if (i < data.struct_fields.length-1) {
$fieldGroup.append(',');
}
$group.append($fieldGroup);
}
}
nesting--;
indent(nesting, $group);
$group.append('<span class="end-obj">}</span>');
break;
case "bool":
$group.append('<span class="bool">false</span>'); // TODO: default value?
break;
case "int":
case "uint":
case "float":
case "complex":
$group.append('<span class="num">0</span>'); // TODO: default value?
break;
case "string":
$group.append('<span class="qu">"</span><span class="str"></span><span class="qu">"</span>'); // TODO: default value?
break;
case "array":
$group.append('[');
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($tpl, module, data.elems, nesting, path, $group);
}
$group.append(']');
break;
case "map":
$group.append('{\n')
nesting++;
renderModuleInlineKey($tpl, module, data, nesting, $group);
indent(nesting, $group);
renderData($tpl, module, data.map_keys, nesting, path, $group);
$group.append(': ');
renderData($tpl, module, data.elems, nesting, path, $group);
$group.append('\n');
nesting--;
indent(nesting, $group);
$group.append('}');
break;
case "module":
case "module_map":
var aTag = '<a';
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;
}
}
function renderModuleInlineKey($tpl, module, data, nesting, $group) {
if (!data.module_inline_key) {
return
}
var moduleName = pathComponents[pathComponents.length-2];
indent(nesting, $group);
$group.append('<span class="qu">"</span><span class="key module-inline-key has-popup">'+data.module_inline_key+'</span><span class="qu">"</span>: <span class="qu">"</span><span class="str"><b>'+moduleName+'</b></span><span class="qu">"</span>');
if (data.struct_fields && data.struct_fields.length > 0) {
$group.append(',');
}
$group.append('\n');
appendToFieldDocs($tpl, module, data.module_inline_key, $('#hovercard-inline-key').html());
}
function appendToFieldDocs($tpl, module, cleanFieldPath, fieldDoc) {
var dt = cleanFieldPath;
if (canTraverse(module)) {
dt = '<a href="./'+cleanFieldPath+'/">'+dt+'</a>';
}
$('.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) {
var $span = $('<span class="indent"></span>');
$span.append('\t'.repeat(nesting));
$group.append($span);
}
function makeSubmoduleList(module, path, value) {
while (value.elems) {
value = value.elems;
}
if (value.type != "module" && value.type != "module_map") {
return '';
}
var submodList = '<ul>';
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;
}
submodList += '<li>'+submodLink+'</li>';
}
}
submodList += '</ul>';
return '<div><p>Fulfilled by modules in namespace: <b>'+value.module_namespace+'</b></p>'+submodList+'</div>';
}
// canTraverse returns true if the page data
// 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(data) {
return data.breadcrumb != null;
}
function newGroup() {
return $('<div class="group"/>');
}
function replaceGoTypeNameWithCaddyModuleName(docs, module, moduleID) {
if (!docs || !moduleID) return docs;
// fully qualified type name
let fqtn = module.structure.type_name;
// extract just the local type name
let {_, typeName} = splitTypeName(fqtn);
// 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 "";
}
return marked(input);
}

View file

@ -0,0 +1,87 @@
$(function() {
function hasPrefix(str, prefix) {
if (!prefix) return true;
if (!str) return false;
return str.indexOf(prefix) === 0;
}
// highlight current page in left nav
var $currentPageLink = $('main nav a[href="'+window.location.pathname+'"]');
if (hasPrefix(window.location.pathname, "/docs/json/")) {
// as a special case, highlight the JSON structure link anywhere within it
$currentPageLink = $('main nav a[href="/docs/json/"]');
}
if (hasPrefix(window.location.pathname, "/docs/modules/")) {
// as another special case, highlight the modules link anywhere within it
$currentPageLink = $('main nav a[href="/docs/modules/"]');
}
$currentPageLink.addClass('current');
// add anchor links, inspired by https://github.com/bryanbraun/anchorjs
$('article > h1[id], article > h2[id], article > h3[id], article > h4[id], article > h5[id], article > h6[id]').each(function() {
var $anchor = $('<a href="#'+this.id+'" class="anchor-link" title="Direct link">🔗</a>');
$(this).append($anchor);
});
// the server-side markdown renderer annoyingly renders
// colored code blocks differently from plain ones, in that
// colorized ones do not have the additional <code> inside
// the <pre>; this line finds those and adds a .chroma class
// to the outer pre element, and our CSS file has a style to
// ensure the inner code block does not produce extra padding
$('article > pre:not(.chroma) > code:not(.cmd)').parent().addClass('chroma');
// Add links to Caddyfile directive tokens in code blocks.
// See include/docs-head.html for the whitelist bootstrapping logic
$('pre.chroma .k')
.filter((k, item) =>
window.CaddyfileDirectives.includes(item.innerText)
|| item.innerText === '<directives...>'
)
.map(function(k, item) {
let text = item.innerText;
let url = text === '<directives...>'
? '/docs/caddyfile/directives'
: '/docs/caddyfile/directives/' + text;
text = text.replace(/</g,'&lt;').replace(/>/g,'&gt;');
$(item).html('<a href="' + url + '" style="color: inherit;" title="Directive">' + text + '</a>');
});
// Add links to [<matcher>] or named matcher tokens in code blocks.
// The matcher text includes <> characters which are parsed as HTML,
// so we must use text() to change the link text.
$('pre.chroma .nd')
.map(function(k, item) {
let text = item.innerText.replace(/</g,'&lt;').replace(/>/g,'&gt;');
$(item).html('<a href="/docs/caddyfile/matchers#syntax" style="color: inherit;" title="Matcher token">' + text + '</a>');
});
});
// addLinkaddLinksToSubdirectivessToAnchors finds all the ID anchors
// in the article, and turns any directive or subdirective span into
// links that have an ID on the page. This is opt-in for each page,
// because it's not necessary to run everywhere.
function addLinksToSubdirectives() {
let anchors = $('article *[id]').map((i, el) => el.id).toArray();
$('pre.chroma .k')
.filter((k, item) => anchors.includes(item.innerText))
.map(function(k, item) {
let text = item.innerText.replace(/</g,'&lt;').replace(/>/g,'&gt;');
let url = '#' + item.innerText;
$(item).html('<a href="' + url + '" style="color: inherit;" title="' + text + '">' + text + '</a>');
});
}
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

@ -0,0 +1,278 @@
// 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;
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 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';
for (var i = 0; i < packageList.length; i++) {
var pkg = packageList[i];
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)) {
$($pkg).addClass("selected");
}
if (pkg.modules && pkg.modules.length > 0) {
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);
}
} else {
$('.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($pkg);
}
updatePage();
});
}).fail(function(jqxhr, status, error) {
swal({
icon: "error",
title: "Unavailable",
content: $('<div>Sorry, the build server is down for maintenance right now. You can try again later or <a href="https://github.com/caddyserver/caddy/releases/latest">download pre-built Caddy binaries from GitHub</a> any time.</div>')[0]
});
$(function() {
disableFields(false);
});
});
$(function() {
autoPlatform();
downloadButtonHtml = $('#download').html();
$('#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++;
});
// 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();
});
$('#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;
}
disableFields(true);
if (typeof fathom !== 'undefined') {
fathom.trackGoal('U9K2UTFV', 0);
}
$.ajax($(this).attr('href'), { method: "HEAD" }).done(function(data, status, jqxhr) {
window.onbeforeunload = null; // disable exit confirmation before "redirecting" to download
window.location = jqxhr.getResponseHeader("Location");
}).fail(function(jqxhr, status, error) {
handleBuildError(jqxhr, status, error);
}).always(function() {
enableFields();
});
return false;
});
})
// autoPlatform chooses the platform in the list that best
// matches the user's browser, if it's available.
function autoPlatform() {
var [os, arch] = detectPlatform();
$('#platform').val(os+"-"+arch);
updatePage();
}
function getDownloadLink() {
// make sure we at least have a default,
// in the case that autoPlatform() failed
var platformString = $('#platform').val();
if (!platformString) {
platformString = "linux-amd64"
}
// get platform components
var [os, arch, arm = ""] = platformString.split("-");
var qs = new URLSearchParams();
if (os) qs.set("os", os);
if (arch) qs.set("arch", arch);
if (arm) qs.set("arm", arm);
// get plugins and their versions
$('#optional-packages .selected').each(function() {
// get package path
var p = $('.package-link', this).text().trim();
// get package version, if user specified one
var ver = $('input[name=version]', this).val().trim();
if (ver) {
p += "@"+ver;
}
qs.append("p", p);
});
$("#darwin-warning").toggle(os === "darwin");
var idempotencyKey = Math.floor(Math.random() * 99999999999999);
qs.append("idempotency", idempotencyKey);
return "/api/download?"+qs.toString();
}
function handleBuildError(jqxhr, status, error) {
var $content = $('<div class="swal-custom-content">');
if (jqxhr.status == 502) {
swal({
icon: "error",
title: "Unavailable",
content: $content.html('Sorry, the build server is down for maintenance right now. You can try again later or <a href="https://github.com/caddyserver/caddy/releases/latest">download pre-built Caddy binaries from GitHub</a>.')[0]
});
} else {
swal({
icon: "error",
title: "Build failed",
content: $content.html('The two most common reasons are:<ol><li><b>A plugin is not compiling.</b> The developer must release a new version that compiles.</li><li><b>The build configuration is invalid.</b> If you specified any versions, make sure they are correct and <a href="https://golang.org/cmd/go/#hdr-Module_compatibility_and_semantic_versioning" target="_blank">within the same major version</a> as the path of the associated package.</li></ol>In the meantime, you can <a href="https://github.com/caddyserver/caddy/releases/latest">download Caddy from GitHub</a> without any plugins.')[0]
});
}
}
function updatePage() {
$('#package-count').text($('.package.selected').length);
$('#download').attr('href', getDownloadLink());
}
function disableFields(building) {
$('#download, #optional-packages').addClass('disabled');
$('.download-bar select, #optional-packages input').prop('disabled', true);
if (building) {
$('#download').html('<div class="loader"></div> Building');
// prevent accidentally leaving the page during a build
window.onbeforeunload = function() {
return "Your custom build is in progress.";
};
} else {
$('#download').html('Builds Unavailable');
}
}
function enableFields() {
$('#download, #optional-packages').removeClass('disabled');
$('.download-bar select, #optional-packages input').prop('disabled', false);
$('#download').html(downloadButtonHtml);
// allow user to leave page easily
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

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,52 @@
const jsonDocsPathPrefix = "/docs/json/";
var configPath = window.location.pathname.substr(jsonDocsPathPrefix.length-1); // keep trailing slash
var pathComponents = configPath.split('/');
setPageTitle();
// load the docs for this path
$.get("/api/docs/config"+configPath, function(json) {
// wait until the DOM has finished loading before rendering the results
$(function() {
beginRenderingInto($('#json-docs-container'), '', json.result);
// establish the breadcrumb
var $bc = $('.breadcrumbs');
$('<a href="'+jsonDocsPathPrefix+'" id="top-breadcrumb">JSON Config Structure</a> &rsaquo;').appendTo($bc);
for (var i = 1; i < pathComponents.length-1; i++) {
var bcPath = pathComponents.slice(0, i+1).join('/');
var bcSiblingPath = pathComponents.slice(1, i).join('/');
// 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);
}
// re-trigger the URL fragment if any, to scroll to the archor
var fragment = window.location.hash;
if (fragment) {
window.location.hash = '';
window.location.hash = fragment;
}
});
});
function setPageTitle() {
// set the page title with something useful
var parts = configPath.split("/");
if (parts.length > 1) {
if (!parts[0]) {
parts.shift();
}
if (!parts[parts.length-1]) {
parts.pop();
}
var titlePrefix = parts.slice(-2).join("/");
if (parts.length > 4) {
titlePrefix = parts.slice(0, 2).join("/")+"/.../"+titlePrefix;
}
if (titlePrefix) {
document.title = titlePrefix + " - " + document.title;
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,92 @@
const moduleDocsPathPrefix = "/docs/modules/";
var moduleID = window.location.pathname.substr(moduleDocsPathPrefix.length);
if (moduleID) {
// 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();
$('.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 {
// 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 infos = moduleList[modID];
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/>');
$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);
});
}
});
});
}

File diff suppressed because one or more lines are too long