// TODO: sanitize all HTML renderings, especially markdown: https://github.com/cure53/DOMPurify var pageDocs = {}; var pageData = {}; var $hovercard; const nonStandardFlag = 'Non-standard'; const standardFlag = 'Standard'; $(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('▸'); } else { // expand $(this).html('▾'); } $(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 =$('
'); 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; } content += ''+truncate(modInfo.docs, 115)+''; $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 = $('').append(' '); for (var j = 0; j < bcSiblings.length; j++) { var sib = bcSiblings[j]; var sibPath = sib.path; if (sibPath) { sibPath += "/"; } sibPath += sib.name+"/"; var aTag = ''+sib.name+''; $siblings.append(aTag); } $('#hovercard-breadcrumb-siblings').html($siblings).show(); } else if ($(this).hasClass('documented')) { // docs var elemDocs = truncate(pageDocs[elemPath], 500); if (!elemDocs) { elemDocs = 'There are no docs for this property.
'; return; } $('#hovercard-docs').html(markdown(elemDocs)).show(); $('#hovercard-inline-link').html('View docs below ↓').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 = $('').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 = 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('{▾'); 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) || 'There are no docs for this property.
'; 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 = ''+field.key+''; $fieldGroup.append('"'+keyATag+'": '); 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('}'); break; case "bool": $group.append('false'); // TODO: default value? break; case "int": case "uint": case "float": case "complex": $group.append('0'); // TODO: default value? break; case "string": $group.append('""'); // TODO: default value? break; case "array": $group.append('['); if (data.elems.type == "module_map") { $group.append('{•••}'); } 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 = '•••'; $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('"'+data.module_inline_key+'": "'+moduleName+'"'); 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 = ''+dt+''; } $('.field-list-contents', $tpl).append('Fulfilled by modules in namespace: '+value.module_namespace+'
'+submodList+'