docs: Add SPA page for listing modules by their ID

This commit is contained in:
Matthew Holt 2020-03-21 21:03:29 -06:00
parent 0834562c9b
commit ca8197d483
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
14 changed files with 565 additions and 392 deletions

View file

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>JSON Structure - Caddy Documentation</title>
<title>JSON Config Structure - Caddy Documentation</title>
{{include "/includes/docs-head.html"}}
<link rel="stylesheet" href="/resources/css/docs-json.css">
<script src="/resources/js/marked-0.8.0.min.js"></script>
<script src="/resources/js/docs-json.js"></script>
<script src="/resources/js/docs-api.js"></script>
<script src="/resources/js/json-docs.js"></script>
</head>
<body>
{{include "/includes/docs-header.html"}}
@ -16,36 +17,16 @@
<!--Populated by JS-->
</div>
<pre><code class="json" id="renderbox"><!--Populated by JS--></code></pre>
{{include "/includes/docs-renderbox.html"}}
<article>
<p id="top-doc">
<!--Populated by JS-->
</p>
<h2 id="field-list-header">Field List</h2>
<dl id="field-list">
<!--Populated by JS-->
</dl>
{{include "/includes/docs-details.html"}}
</article>
</div>
<div class="sidebar"></div>
</main>
<div id="hovercard" class="arrow-box">
<div id="hovercard-docs" class="hovercard-elem"><!--Populated by JS--></div>
<div id="hovercard-module" class="hovercard-elem">
<p id="hovercard-namespace-box">
Fulfilled by modules in namespace: <span id="hovercard-namespace"><!--Populated by JS--></span>
</p>
<div id="hovercard-module-list"><!--Populated by JS--></div>
</div>
<div id="hovercard-inline-key" class="hovercard-elem">
<p class="explain">This property is <b>required</b> because it specifies the module name.</p>
</div>
<div id="hovercard-breadcrumb-siblings" class="hovercard-elem"><!--Populated by JS--></div>
<div id="hovercard-inline-link" class="hovercard-elem"><!--Populated by JS--></div>
</div>
{{include "/includes/docs-hovercard.html"}}
{{include "/includes/footer.html"}}
</body>

View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<title>Modules - Caddy Documentation</title>
{{include "/includes/docs-head.html"}}
<link rel="stylesheet" href="/resources/css/docs-json.css">
<script src="/resources/js/marked-0.8.0.min.js"></script>
<script src="/resources/js/docs-api.js"></script>
<script src="/resources/js/module-docs.js"></script>
</head>
<body>
{{include "/includes/docs-header.html"}}
<main>
{{include "/includes/docs-nav.html"}}
<div class="article-container">
<div id="module-list-container">
<article>
<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>
</div>
<div id="module-docs-container">
<article>
<h1 id="module-name"><!--Populated by JS--></h1>
</article>
{{include "/includes/docs-renderbox.html"}}
<article>
{{include "/includes/docs-details.html"}}
</article>
</div>
</div>
<div class="sidebar"></div>
</main>
{{include "/includes/docs-hovercard.html"}}
{{include "/includes/footer.html"}}
</body>
</html>

View file

@ -0,0 +1,7 @@
<p id="top-doc">
<!--Populated by JS-->
</p>
<h2 id="field-list-header">Field List</h2>
<dl id="field-list">
<!--Populated by JS-->
</dl>

View file

@ -0,0 +1,14 @@
<div id="hovercard" class="arrow-box">
<div id="hovercard-docs" class="hovercard-elem"><!--Populated by JS--></div>
<div id="hovercard-module" class="hovercard-elem">
<p id="hovercard-namespace-box">
Fulfilled by modules in namespace: <span id="hovercard-namespace"><!--Populated by JS--></span>
</p>
<div id="hovercard-module-list"><!--Populated by JS--></div>
</div>
<div id="hovercard-inline-key" class="hovercard-elem">
<p class="explain">This property is <b>required</b> because it specifies the module name.</p>
</div>
<div id="hovercard-breadcrumb-siblings" class="hovercard-elem"><!--Populated by JS--></div>
<div id="hovercard-inline-link" class="hovercard-elem"><!--Populated by JS--></div>
</div>

View file

@ -30,6 +30,7 @@
<li><a href="/docs/caddyfile/options">Global options</a></li>
</ul>
</li>
<li><a href="/docs/modules/">Modules</a></li>
<li><a href="/docs/json/">JSON Config Structure</a></li>
<li><a href="/docs/automatic-https">Automatic HTTPS</a></li>

View file

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

View file

@ -198,7 +198,7 @@ article li {
h1 {
font-size: 80px;
color: #196165;
margin-bottom: 25px;
margin-bottom: 50px;
}
h2 {
@ -352,12 +352,13 @@ main > .sidebar:not(:last-child) {
.json .str { color: #2f8598; }
.json .num { color: #038a3f; }
.json .bool { color: #9b5e14; }
.json .key a:not(:hover) { color: inherit; }
.json .key a:not([href]) { color: #222; }
.json a {
text-decoration: none;
font-weight: bold;
}
.json .has-popup { border-bottom: 1px dashed #1c82dc; }
.json .has-popup { border-bottom: 1px dashed #222; }
.json a[href].has-popup { border-bottom-color: #1c82dc; }
.json .has-popup.module { border-bottom: none; }
@ -567,6 +568,51 @@ td code {
font-size: 14px;
word-wrap: break-word;
}
#module-docs-container,
#module-list-container {
display: none;
}
#module-list {
margin-top: 50px;
}
#module-list td:first-child {
word-wrap: break-word;
max-width: 300px;
}
#module-list .module-link {
font-weight: bold;
font-size: 20px;
}
@media (max-width: 1400px) {
article aside {

View file

@ -0,0 +1,345 @@
// TODO: sanitize all HTML renderings, especially markdown: https://github.com/cure53/DOMPurify
var pageData = {}, pageDocs = {};
var $renderbox, $hovercard;
$(function() {
$renderbox = $('#renderbox');
$hovercard = $('#hovercard');
var hoverTimeout;
$hovercard.hover(function() {
clearTimeout(hoverTimeout);
}, function() {
clearTimeout(hoverTimeout);
$hovercard.removeClass('popup');
});
// toggle an object as expanded or collapsed
$('#renderbox').on('click', '.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).position();
// 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.position().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 (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;
$list.append('<a href="'+href+'" class="module-link">'+modInfo.name+'<span class="module-link-description">'+truncate(modInfo.docs, 115)+'</span></a>');
}
}
$('#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 = pageData.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})
}
break;
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];
bcSiblings.push({name: mod.name, path: siblingPath})
}
}
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+"/";
$siblings.append('<a href="'+jsonDocsPathPrefix+sibPath+'">'+sib.name+'</a>');
}
$('#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 beginRendering(json) {
pageData = json;
console.log("DATA:", pageData);
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));
}
$('#top-doc').append(makeSubmoduleList("", pageData.structure));
renderData(pageData.structure, 0, "", $('<div class="group"/>'));
console.log("DOCS:", pageDocs);
if ($('#field-list').html().trim()) {
$('#field-list-header').show();
}
}
function renderData(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);
$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(fieldPath, field.value);
appendToFieldDocs(cleanFieldPath, fieldDoc);
// render the field to the JSON box
var $fieldGroup = $('<div class="group"/>');
indent(nesting, $fieldGroup);
var keyATag = '<a ';
if (canTraverse()) {
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);
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(data.elems, nesting, path, $group);
}
$group.append(']');
break;
case "map":
$group.append('{\n')
nesting++;
renderModuleInlineKey(data, nesting, $group);
indent(nesting, $group);
renderData(data.map_keys, nesting, path, $group);
$group.append(': ');
renderData(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()) {
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) {
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(data.module_inline_key, $('#hovercard-inline-key').html());
}
function appendToFieldDocs(cleanFieldPath, fieldDoc) {
var dt = cleanFieldPath;
if (canTraverse()) {
dt = '<a href="./'+cleanFieldPath+'/">'+dt+'</a>';
}
$('#field-list').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 truncate(str, len) {
if (!str) return "";
var startLen = str.length;
str = str.substring(0, len);
if (startLen > len) {
str += "...";
}
return str;
}
function makeSubmoduleList(path, value) {
while (value.elems) {
value = value.elems;
}
if (value.type != "module" && value.type != "module_map") {
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;
submodList += '<li><a href="'+href+'">'+submod.name+'</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() {
return pageData.breadcrumb != null;
}
function markdown(input) {
if (!input) {
return "";
}
return marked(input);
}

View file

@ -1,359 +0,0 @@
// TODO: sanitize all HTML renderings, especially markdown: https://github.com/cure53/DOMPurify
function markdown(input) {
if (!input) {
return "";
}
return marked(input);
}
$(function() {
var $renderbox = $('#renderbox');
var $hovercard = $('#hovercard');
var pageDocs = {}, pageData = {};
const jsonDocsPathPrefix = "/docs/json/";
var configPath = window.location.pathname.substr(jsonDocsPathPrefix.length-1); // keep trailing slash
// 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;
}
}
var hoverTimeout;
$hovercard.hover(function() {
clearTimeout(hoverTimeout);
}, function() {
clearTimeout(hoverTimeout);
$hovercard.removeClass('popup');
});
$('body').on({
mouseenter: function() {
// don't allow hoverbox to close anymore, we're re-opening it
clearTimeout(hoverTimeout);
var pos = $(this).position();
// 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.position().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 (pageData.namespaces && pageData.namespaces[modNamespace]) {
for (var i = 0; i < pageData.namespaces[modNamespace].length; i++) {
var modInfo = pageData.namespaces[modNamespace][i];
$list.append('<a href=".'+elemPath+'/'+modInfo.name+'/" class="module-link">'+modInfo.name+'<span class="module-link-description">'+truncate(modInfo.docs, 115)+'</span></a>');
}
}
$('#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 = pageData.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})
}
break;
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];
bcSiblings.push({name: mod.name, path: siblingPath})
}
}
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+"/";
$siblings.append('<a href="'+jsonDocsPathPrefix+sibPath+'">'+sib.name+'</a>');
}
$('#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');
var pathComponents = configPath.split('/');
// load the docs for this path
$.get("/api/docs/config"+configPath, function(json) {
pageData = json;
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));
}
$('#top-doc').append(makeSubmoduleList("", pageData.structure));
console.log("DATA:", pageData);
renderData(pageData.structure, 0, "", $('<div class="group"/>'));
console.log("DOCS:", pageDocs);
if ($('#field-list').html().trim()) {
$('#field-list-header').show();
}
// 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('/');
// 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);
}
});
function renderData(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);
$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(fieldPath, field.value);
appendToFieldDocs(cleanFieldPath, fieldDoc);
// render the field to the JSON box
var $fieldGroup = $('<div class="group"/>');
indent(nesting, $fieldGroup);
$fieldGroup.append('<span class="qu">"</span><span class="key"><a href=".'+fieldPath+'/" data-path="'+fieldPath+'" class="'+linkClass+'">'+field.key+'</a></span><span class="qu">"</span>: ');
renderData(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(data.elems, nesting, path, $group);
}
$group.append(']');
break;
case "map":
$group.append('{\n')
nesting++;
renderModuleInlineKey(data, nesting, $group);
indent(nesting, $group);
renderData(data.map_keys, nesting, path, $group);
$group.append(': ');
renderData(data.elems, nesting, path, $group);
$group.append('\n');
nesting--;
indent(nesting, $group);
$group.append('}');
break;
case "module":
// TODO:
$group.append('{<a href=".'+path+'/" class="module has-popup" data-namespace="'+(data.module_namespace || '')+'" data-path="'+path+'">&bull;&bull;&bull;</a>}');
break;
case "module_map":
// TODO:
$group.append('{<a href=".'+path+'/" class="module has-popup" data-namespace="'+(data.module_namespace || '')+'" data-path="'+path+'">&bull;&bull;&bull;</a>}');
break;
}
$renderbox.append($group);
}
function renderModuleInlineKey(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(data.module_inline_key, $('#hovercard-inline-key').html());
}
function appendToFieldDocs(cleanFieldPath, fieldDoc) {
$('#field-list').append('<dt class="field-name" id="'+cleanFieldPath+'"><a href="#'+cleanFieldPath+'" class="inline-link">&#128279;</a><a href="./'+cleanFieldPath+'/">'+cleanFieldPath+'</a></dt> <dd>'+fieldDoc+'</dd>');
}
// toggle an object as expanded or collapsed
$('#renderbox').on('click', '.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');
});
function makeSubmoduleList(path, value) {
while (value.elems) {
value = value.elems;
}
if (value.type != "module" && value.type != "module_map") {
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];
submodList += '<li><a href=".'+path+'/'+submod.name+'/">'+submod.name+'</li>';
}
}
submodList += '</ul>';
return '<div><p>Fulfilled by modules in namespace: <b>'+value.module_namespace+'</b></p>'+submodList+'</div>';
}
function indent(nesting, $group) {
var $span = $('<span class="indent"></span>');
$span.append('\t'.repeat(nesting));
$group.append($span);
}
function truncate(str, len) {
if (!str) return "";
var startLen = str.length;
str = str.substring(0, len);
if (startLen > len) {
str += "...";
}
return str;
}
});

View file

@ -11,6 +11,10 @@ $(function() {
// 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

View file

@ -0,0 +1,46 @@
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() {
beginRendering(json);
// 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('/');
// 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);
}
});
});
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;
}
}
}

View file

@ -0,0 +1,31 @@
const moduleDocsPathPrefix = "/docs/modules/";
var moduleID = window.location.pathname.substr(moduleDocsPathPrefix.length);
if (moduleID) {
// update page title and load the docs for this module
document.title = "Module " + moduleID + " - Caddy Documentation";
$.get("/api/docs/module/"+moduleID, function(json) {
// wait until the DOM has finished loading before rendering the results
$(function() {
$('#module-docs-container').show();
$('h1').text("Module "+moduleID);
beginRendering(json);
});
});
} else {
// populate the module list
$.get("/api/modules", function(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 doc = moduleList[modID];
var $tr = $('<tr/>');
$tr.append('<td><a href="./'+modID+'" class="module-link">'+modID+'</a></td>');
$tr.append('<td>'+markdown(truncate(doc, 200))+'</td>');
$table.append($tr);
}
});
});
}