@typhonjs-typedoc/typedoc-theme-dmt
Version:
Provides a modern and customizable UX augmentation to the default TypeDoc theme bringing enhanced features and usability.
1,496 lines (1,280 loc) • 179 kB
JavaScript
/**
* @module @typhonjs-typedoc/typedoc-theme-dmt
* @license MPL-2.0
* @see https://github.com/typhonjs-typedoc/typedoc-theme-dmt
*/
import fs from 'node:fs';
import path from 'node:path';
import { ReflectionKind, PageEvent, RendererEvent, DeclarationReflection, DocumentReflection, IndexEvent, ProjectReflection, DefaultTheme, ParameterType, Application, Converter } from 'typedoc';
import { fileURLToPath, URL } from 'node:url';
import { load as load$1 } from 'cheerio';
import lunr from 'lunr';
/**
* Provides a basic mechanism to walk and query the TypeDoc `NavigationElement` tree structure.
*/
class NavigationTree
{
/**
* Searches the navigation index for the given path URL and performs the given operation on each tree node from the
* path if found.
*
* @param {import('typedoc').NavigationElement[] } tree - The root tree node to walk.
*
* @param {string} pathURL - The path URL to locate.
*
* @param {import('./types').TreeOperation} operation - Tree entry operation to apply.
*
* @returns {boolean} If the path is found and operation is applied.
*/
static searchPath(tree, pathURL, operation)
{
if (!tree?.length) { return false; }
// Scan all top level entries first.
for (const entry of tree)
{
if (Array.isArray(entry.children)) { continue; }
// If the path is found at the top level do nothing and return early.
if (entry?.path === pathURL) { return true; }
}
// Depth first search for path executing `operation` if found.
for (const entry of tree)
{
if (!Array.isArray(entry.children)) { continue; }
if (this.#searchPath(entry, pathURL, operation)) { return true; }
}
return false;
}
/**
* Recursively walks the navigation index / tree for just tree nodes invoking the given operation.
*
* @param {import('typedoc').NavigationElement[] } tree - The root tree node to walk.
*
* @param {import('./types').TreeOperation} operation - Tree entry operation to apply.
*/
static walk(tree, operation)
{
// Depth first search for path setting a new variable `opened` for all leaves up to path entry.
for (const entry of tree)
{
if (!Array.isArray(entry.children)) { continue; }
this.#walkPath(entry, void 0, operation);
}
}
/**
* Recursively walks the navigation index / tree for just tree nodes invoking the given operation from the given
* `entry`.
*
* @param {import('typedoc').NavigationElement} entry - The current entry.
*
* @param {import('./types').TreeOperation} operation - Tree entry operation to apply.
*/
static walkFrom(entry, operation)
{
this.#walkPath(entry, void 0, operation);
}
// Internal implementation ----------------------------------------------------------------------------------------
/**
* Helper function to recursively search for the path and perform the operation given for each tree node.
*
* @param {import('typedoc').NavigationElement} entry - Current NavigationElement.
*
* @param {string} pathURL - The path URL to locate.
*
* @param {import('./types').TreeOperation} operation - Tree entry operation to apply.
*
* @returns {boolean} Whether the path URL matched an entry in this branch.
*/
static #searchPath(entry, pathURL, operation)
{
// If the path matches, return true to indicate the path has been found.
if (entry.path === pathURL) { return true; }
// If the entry has children, continue the search recursively.
if (Array.isArray(entry.children))
{
for (const child of entry.children)
{
const found = this.#searchPath(child, pathURL, operation);
if (found)
{
operation({ entry });
return true;
}
}
}
// If the path has not been found in this branch, return false.
return false;
}
/**
* Walks the navigation index / tree for each path recursively.
*
* @param {import('typedoc').NavigationElement} entry - The current entry.
*
* @param {import('typedoc').NavigationElement} parentEntry - The parent entry.
*
* @param {import('./types').TreeOperation} operation - Tree entry operation to apply.
*/
static #walkPath(entry, parentEntry, operation)
{
// If the entry has children, continue the search recursively.
if (Array.isArray(entry.children))
{
for (const child of entry.children) { this.#walkPath(child, entry, operation); }
}
operation({ entry, parentEntry });
}
}
/**
* Parses the default theme navigation index module reflections creating a tree structure with an optional max depth
* before concatenating paths. A `depthMax` of 0 results in the same output as the default theme / full paths per
* module.
*/
class ModuleTreeMap
{
/**
* The maximum depth before concatenating node paths.
*
* @type {number}
*/
#depthMax = Number.MAX_SAFE_INTEGER;
/**
* @type {string}
*/
#packageName;
/**
* @type {MapNode}
*/
#root = new MapNode();
/**
* @param {number} depthMax - Max depth before path concatenation.
*
* @param {string} [packageName] Any associated package name.
*/
constructor(depthMax, packageName)
{
this.#depthMax = depthMax;
this.#packageName = packageName;
}
/**
* Adds a NavigationElement to the tree recursively adding intermediary nodes by splitting on `/`.
*
* If there is an associated `packageName` from the ProjectReflection then splitting will respect the package name
* which is useful for packages that are organization based `@org-name/package-name`.
*
* @param {import('typedoc').NavigationElement} node - NavigationElement to add.
*/
add(node)
{
const path = node.text;
if (typeof path !== 'string') { return; }
let pathSegments;
// Add an extra trailing slash to `packageName` to detect continuation parsing.
const packageNameExtra = typeof this.#packageName === 'string' ? `${this.#packageName}/` : void 0;
if (path === this.#packageName)
{
pathSegments = [this.#packageName];
}
else if (packageNameExtra && path.startsWith(packageNameExtra))
{
pathSegments = [this.#packageName];
pathSegments.push(...path.substring(packageNameExtra.length).split('/'));
}
else
{
pathSegments = path.split('/');
}
this.#addToMap(this.#root, pathSegments, node, 0);
}
/**
* Recursively adds intermediary MapNode instances for all path segments setting to `node` field for leafs.
*
* @param {MapNode} currentMap - Current map node.
*
* @param {string[]} pathSegments - All path segments parsed from original module node `text`.
*
* @param {import('typedoc').NavigationElement} node - Node to add.
*
* @param {number} currentDepth - Current depth.
*/
#addToMap(currentMap, pathSegments, node, currentDepth)
{
if (pathSegments.length === 0)
{
node.text = '';
currentMap.node = node;
return;
}
const currentSegment = pathSegments.shift();
if (!currentMap.map.get(currentSegment))
{
currentMap.map.set(currentSegment, new MapNode());
}
this.#addToMap(currentMap.map.get(currentSegment), pathSegments, node, currentDepth + 1);
}
/**
* Constructs the output tree from the MapNode structure considering `depthMax`.
*
* @returns {import('typedoc').NavigationElement[]} - The root of the reconstituted tree structure.
*/
buildTree()
{
return this.#buildTree(this.#root, 0, void 0).children;
}
/**
* Handles recursively building the output module tree.
*
* @param {MapNode} currentMapNode - Node being processed.
*
* @param {number} currentDepth - Current depth.
*
* @param {string} pathPrefix - Path concatenation after `depthMax` exceeded.
*
* @param {import('typedoc').NavigationElement} depthMaxParent - The target parent NavigationElement to append leaf
* nodes to after `depthMax` exceeded.
*
* @returns {import('typedoc').NavigationElement | undefined} A fabricated intermediary NavigationElement or a leaf
* node.
*/
#buildTree(currentMapNode, currentDepth, pathPrefix, depthMaxParent = void 0)
{
if (!currentMapNode) { return; }
let node;
// Create a node if currentMapNode has a node or if depth is not beyond depthMax.
if (currentMapNode.node || currentDepth <= this.#depthMax)
{
node = currentMapNode.node ? currentMapNode.node : { text: pathPrefix, children: [] };
node.text = pathPrefix;
}
// Set `depthMaxParent` at the `depthMax` level. All subsequent leaf nodes will be added to this parent node.
if (currentDepth === this.#depthMax)
{
depthMaxParent = node;
}
for (const [key, mapNode] of currentMapNode.map.entries())
{
let childNode;
if (currentDepth <= this.#depthMax)
{
childNode = this.#buildTree(mapNode, currentDepth + 1, key, depthMaxParent);
}
else
{
// Concatenate paths after `depthMax` is exceeded.
const newPrefix = pathPrefix ? `${pathPrefix}/${key}` : key;
childNode = this.#buildTree(mapNode, currentDepth + 1, newPrefix, depthMaxParent);
}
if (childNode)
{
if (currentDepth >= this.#depthMax && depthMaxParent)
{
// After max depth is reached only add leaf nodes that define a reflection `kind`.
if (childNode.kind)
{
// Ensure `depthMaxParent` has a `children` array.
if (!Array.isArray(depthMaxParent.children)) { depthMaxParent.children = []; }
depthMaxParent.children.unshift(childNode);
}
}
else if (node)
{
// Ensure `node` has a `children` array.
if (!Array.isArray(node.children)) { node.children = []; }
node.children.unshift(childNode);
}
}
}
return node;
}
/**
* Post-processes a NavigationElement tree concatenating singular paths across multiple levels.
*
* @param {import('typedoc').NavigationElement[]} tree - The built tree output of `buildTree`.
*
* @returns {import('typedoc').NavigationElement[]} The tree with singular paths concatenated.
*/
static compactSingularPaths(tree)
{
return tree.map((node) => this.#compactSingularPaths(node));
}
/**
* Post-processes a single NavigationElement branch to concatenate singular paths across multiple levels.
*
* @param {import('typedoc').NavigationElement} node - The current node being processed.
*
* @returns {import('typedoc').NavigationElement} The processed node.
*/
static #compactSingularPaths(node)
{
// Early out for leaf nodes.
if (!node || !node.children || node.children.length === 0 || node?.kind !== void 0) { return node; }
// Recursively process all children first.
node.children = node.children.map((child) => this.#compactSingularPaths(child));
// Potentially compact current node if there is only one child.
if (node.children.length === 1)
{
const child = node.children[0];
// If the single child is a non-leaf node concatenate the path text and lift child node.
if (child?.children?.length > 0)
{
node.text += `/${child.text}`;
node.path = child.path;
node.kind = child.kind;
node.class = child.class;
node.children = child.children;
}
}
return node;
}
}
/**
* Defines a tree node which may have a map of children MapNodes and / or an associated NavigationElement.
*/
class MapNode
{
/** @type {Map<string, MapNode>} */
map = new Map();
/** @type {import('typedoc').NavigationElement} | undefined} */
node = void 0;
}
/**
* Manages modifications to the default theme navigation index.
*/
class NavigationIndex
{
/**
* @type {DMTNavigationIndex} Processed navigation index.
*/
static #data = { markdown: [], source: [] };
/**
* The module NavigationElement name for the fabricated tree index.
*
* @type {string}
*/
static #markdownIndexName = '';
/**
* Project package name.
*
* @type {string}
*/
static #packageName = '';
/**
* Project name.
*
* @type {string}
*/
static #projectName = '';
/**
* @returns {DMTNavigationIndex} Processed navigation index.
*/
static get data()
{
return this.#data;
}
/**
* @returns {boolean} Whether there is Markdown document data.
*/
static get hasMarkdown()
{
return this.#data.markdown.length > 0;
}
/**
* @returns {boolean} Whether there is source data.
*/
static get hasSource()
{
return this.#data.source.length > 0;
}
/**
* @returns {string} The module NavigationElement name for the fabricated tree index.
*/
static get markdownIndexName()
{
return this.#markdownIndexName;
}
/**
* @returns {string} Returns the project package name.
*/
static get packageName()
{
return this.#packageName;
}
/**
* @returns {string} Returns the project name.
*/
static get projectName()
{
return this.#projectName;
}
/**
* @param {import('typedoc').Application} app -
*
* @param {import('typedoc').ProjectReflection} project -
*
* @param {ThemeOptions} options - Theme options.
*
* @returns {({
* markdown: import('typedoc').NavigationElement[],
* source: import('typedoc').NavigationElement[]
* })} Processed navigation index.
*/
static transform(app, project, options)
{
/** @type {import('typedoc').NavigationElement[]} */
const index = app.renderer.theme?.getNavigation?.(project) ?? [];
this.#packageName = project?.packageName;
this.#projectName = project?.name;
const markdown = this.#parseMarkdownTree(app, index);
// No processing necessary so directly return the index.
const tree = options.navigation.style === 'flat' ? index : this.#parseModuleTree(index, options,
this.#packageName);
this.#data = {
markdown,
source: options.navigation.style === 'compact' ? ModuleTreeMap.compactSingularPaths(tree) : tree
};
return this.#data;
}
// Internal Implementation ----------------------------------------------------------------------------------------
/**
* @param {import('typedoc').NavigationElement} node - Node to query.
*
* @returns {boolean} True if the Node is a folder only node with children.
*/
static #isFolder(node)
{
return typeof node.text === 'string' && Array.isArray(node.children) && node.kind === void 0;
}
/**
* @param {import('typedoc').NavigationElement} node -
*
* @param {import('typedoc').ReflectionKind} kind -
*
* @returns {boolean} Returns true if `node` is a folder and contains only children that match the reflection kind.
*/
static #isFolderAllOfKind(node, kind)
{
if (!this.#isFolder(node)) { return false; }
let allOfKind = true;
/**
* @type {import('#shared/types').TreeOperation}
*/
const operation = ({ entry }) =>
{
if (entry.kind === void 0) { return; }
if (entry.kind !== kind) { allOfKind = false; }
};
NavigationTree.walkFrom(node, operation);
return allOfKind;
}
/**
* Parses and separates top level Markdown documents from the main navigation index. Due to the various default
* options there are various parsing options to handle.
*
* @param {import('typedoc').Application} app -
*
* @param {import('typedoc').NavigationElement[]} index - The original navigation index.
*
* @returns {import('typedoc').NavigationElement[]} Separated Markdown navigation index.
*/
static #parseMarkdownTree(app, index)
{
const markdownIndex = [];
const navigation = app.options.getValue('navigation');
const categorizeByGroup = app.options.getValue('categorizeByGroup');
if (navigation?.includeGroups && navigation?.includeCategories && categorizeByGroup)
{
this.#parseMarkdownTreeWithGroups(index, markdownIndex);
}
else if (navigation?.includeCategories)
{
if (categorizeByGroup)
{
this.#parseMarkdownTreeNoCategoriesGroups(index, markdownIndex);
}
else
{
this.#parseMarkdownTreeWithCategories(index, markdownIndex);
}
}
else if (navigation?.includeGroups)
{
this.#parseMarkdownTreeWithGroups(index, markdownIndex);
}
else if (!navigation?.includeGroups)
{
this.#parseMarkdownTreeNoCategoriesGroups(index, markdownIndex);
}
return markdownIndex;
}
/**
* @param {import('typedoc').NavigationElement[]} index - The original navigation index.
*
* @param {import('typedoc').NavigationElement[]} markdownIndex - The target tree to separate into.
*/
static #parseMarkdownTreeNoCategoriesGroups(index, markdownIndex)
{
// Determine if there is a top level "Modules" group node. This is the case when Typedoc option
// `navigation: { includeGroups: true }` is set.
for (let i = index.length; --i >= 0;)
{
const node = index[i];
if (node?.kind === ReflectionKind.Document)
{
markdownIndex.unshift(node);
index.splice(i, 1);
}
}
// Remove TypeDoc fabricated root module when there are markdown files present in the index.
if (markdownIndex.length && index.length === 1 && index[0]?.kind === ReflectionKind.Module)
{
const children = index[0]?.children ?? [];
const markdownIndexNode = index.pop();
this.#markdownIndexName = markdownIndexNode.text;
index.push(...children);
}
}
/**
* @param {import('typedoc').NavigationElement[]} index - The original navigation index.
*
* @param {import('typedoc').NavigationElement[]} markdownIndex - The target tree to separate into.
*/
static #parseMarkdownTreeWithCategories(index, markdownIndex)
{
// Parse top level nodes.
for (let i = index.length; --i >= 0;)
{
const node = index[i];
// If all children entries are documents then splice entire folder.
if (this.#isFolderAllOfKind(node, ReflectionKind.Document))
{
markdownIndex.unshift(node);
index.splice(i, 1);
continue;
}
// Otherwise separate out Markdown documents from any folder children.
if (this.#isFolder(node))
{
const children = node.children ?? [];
const childrenDocuments = [];
for (let j = children.length; --j >= 0;)
{
const childNode = children[j];
if (childNode.kind === ReflectionKind.Document)
{
childrenDocuments.unshift(childNode);
children.splice(j, 1);
}
}
if (childrenDocuments.length)
{
markdownIndex.unshift(Object.assign({}, node, { children: childrenDocuments }));
}
}
}
// Remove remaining `Other` category folder if it only contains modules or if this is a fabricated `index`
// module.
if (index.length === 1 && this.#isFolder(index[0]))
{
const children = index[0]?.children ?? [];
let allModules = true;
for (const node of children)
{
if (node.kind !== ReflectionKind.Module) { allModules = false; }
}
if (allModules)
{
// Remove all nodes.
index.length = 0;
// There is a single child matching the fabricated `index` module when markdown files are present.
if (markdownIndex.length && children.length === 1 && children[0]?.kind === ReflectionKind.Module &&
children[0]?.text === 'index')
{
index.push(...children[0]?.children ?? []);
}
else
{
// Add all children modules to top level.
index.push(...children);
}
}
}
}
/**
* @param {import('typedoc').NavigationElement[]} index - The original navigation index.
*
* @param {import('typedoc').NavigationElement[]} markdownIndex - The target tree to separate into.
*/
static #parseMarkdownTreeWithGroups(index, markdownIndex)
{
// Parse top level nodes.
for (let i = index.length; --i >= 0;)
{
const node = index[i];
// If all children entries are documents then splice entire folder.
if (this.#isFolderAllOfKind(node, ReflectionKind.Document))
{
markdownIndex.unshift(node);
index.splice(i, 1);
}
}
// Remove TypeDoc fabricated root module when there are markdown files present in the index.
if (index.length === 1 && this.#isFolder(index[0]))
{
const children = index[0]?.children ?? [];
let allModules = true;
for (const node of children)
{
if (node.kind !== ReflectionKind.Module) { allModules = false; }
}
if (allModules)
{
index.length = 0;
// There are Markdown documents and there is a single child matching the fabricated `index` module when
// markdown files are present.
if (markdownIndex.length && children.length === 1 &&
children[0]?.kind === ReflectionKind.Module && children[0]?.text === 'index')
{
index.push(...children[0]?.children ?? []);
}
else
{
index.push(...children);
}
}
}
}
/**
* @param {import('typedoc').NavigationElement[]} index - Navigation index.
*
* @param {ThemeOptions} options - Theme options.
*
* @param {string} [packageName] Any associated package name.
*
* @returns {import('typedoc').NavigationElement[]} Processed navigation index.
*/
static #parseModuleTree(index, options, packageName)
{
const moduleTreeMap = new ModuleTreeMap(Number.MAX_SAFE_INTEGER, packageName);
let moduleGroup;
// Determine if there is a top level "Modules" group node. This is the case when Typedoc option
// `navigation: { includeGroups: true }` is set.
for (let i = index.length; --i >= 0;)
{
const node = index[i];
if (node?.text === 'Modules' && Array.isArray(node.children) &&
node.children?.[0]?.kind === ReflectionKind.Module)
{
moduleGroup = node;
// Potentially change text to `Packages` given `isPackage` option.
if (options.moduleRemap.isPackage) { moduleGroup.text = 'Packages'; }
break;
}
}
// Handle the case when modules are in the module group node.
if (moduleGroup)
{
for (let i = moduleGroup.children.length; --i >= 0;)
{
const node = moduleGroup.children[i];
// For all module NavigationElements add to the module tree map and remove original from navigation index.
if (node.kind === ReflectionKind.Module)
{
moduleTreeMap.add(node);
moduleGroup.children.splice(i, 1);
}
}
moduleGroup.children.unshift(...moduleTreeMap.buildTree());
}
else // Filter the main index.
{
for (let i = index.length; --i >= 0;)
{
const node = index[i];
// For all module NavigationElements add to the module tree map and remove original from navigation index.
if (node.kind === ReflectionKind.Module)
{
moduleTreeMap.add(node);
index.splice(i, 1);
}
}
index.unshift(...moduleTreeMap.buildTree());
}
return index;
}
}
class PageRenderer
{
/** @type {import('typedoc').Application} */
#app;
/** @type {ThemeOptions} */
#options;
/**
* @param {import('typedoc').Application} app -
*
* @param {ThemeOptions} options -
*/
constructor(app, options)
{
this.#app = app;
this.#options = options;
this.#app.renderer.on(PageEvent.END, this.#handlePageEnd.bind(this));
}
/**
* Adds a script links to load DMT source bundles. Also removes TypeDoc scripts as necessary (search).
*
* @param {import('cheerio').Cheerio} $ -
*
* @param {PageEvent} page -
*/
#addAssets($, page)
{
// Get asset path to script by counting the number of `/` characters then building the relative path.
const count = (page.url.match(/\//) ?? []).length;
const basePath = '../'.repeat(count);
const headEl = $('head');
// Append stylesheet to the head element.
headEl.append($(`<link rel="stylesheet" href="${basePath}assets/dmt/dmt-components.css" />`));
headEl.append($(`<link rel="stylesheet" href="${basePath}assets/dmt/dmt-theme.css" />`));
// Append DMT components script to the head element.
headEl.append($(`<script src="${basePath}assets/dmt/dmt-components.js" type="module" />`));
if (this.#options?.favicon?.url)
{
// Append favicon URL to the head element.
headEl.append($(`<link rel="icon" href="${this.#options.favicon.url}" />`));
}
else if (this.#options?.favicon?.filename)
{
// Append favicon to the head element.
headEl.append($(`<link rel="icon" href="${basePath}${this.#options.favicon.filename}" />`));
}
// To reduce flicker from loading `main.js` and additional Svelte components make `body` start with no visibility.
headEl.prepend('<style>body { visibility: hidden; }</style>');
// For no Javascript loading reverse the above style on load. The main Svelte component bundle will reverse this
// style after all components have been loaded on a requestAnimationFrame callback.
headEl.append('<noscript><style>body { visibility: visible; }</style></noscript>');
}
/**
* Modifications for every page.
*
* @param {import('cheerio').Cheerio} $ -
*/
#augmentGlobal($)
{
// Move header, container-main, and footer elements into the `main` element ------------------------------------
const bodyEl = $('body');
bodyEl.append('<main></main>');
$('body > header, body > .container-main, body > footer').appendTo('body > main');
const hideGenerator = this.#app.options.getValue('hideGenerator');
const customFooterHtml = this.#app.options.getValue('customFooterHtml');
// Remove the footer if there is no content.
if (hideGenerator && !customFooterHtml) { $('body main footer').remove(); }
// Add DMT link to any generator footer ------------------------------------------------------------------------
const generatorEl = $('footer .tsd-generator');
if (generatorEl)
{
generatorEl.html(`${generatorEl.html()} with the <a href="https://www.npmjs.com/package/@typhonjs-typedoc/typedoc-theme-dmt" target="_blank">Default Modern Theme</a>`);
}
// Replace inline script content removing unnecessary style `display` gating for page display. -----------------
const inlineScript = $('body script:first-child');
const scriptContent = inlineScript?.text();
if (scriptContent?.includes('document.documentElement.dataset.theme'))
{
// Replace the script content
inlineScript.text('document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";');
}
// Wrap the title header in a flex box to allow additional elements to be added right aligned. -----------------
const titleEl = $('.tsd-page-title h1');
titleEl.wrap('<div class="dmt-title-header-flex"></div>');
// Replace default main search with DMT main search ------------------------------------------------------------
// Empty default theme search div to make space for the DMT search component.
const tsdSearchEl = $($('#tsd-search-field').parent());
tsdSearchEl.attr('id', 'dmt-search-main');
tsdSearchEl.empty();
// Remove default theme search results.
$('ul.results').remove();
// Replace default toolbar links with DMT toolbar links --------------------------------------------------------
// Empty and assign a new ID to default theme toolbar links.
const tsdToolbarEl = $($('#tsd-toolbar-links').parent());
tsdToolbarEl.attr('id', 'dmt-toolbar');
tsdToolbarEl.empty();
// Clone title anchor and append to #dmt-toolbar.
const tsdTitleEl = $('#tsd-search a.title');
tsdToolbarEl.append(tsdTitleEl.clone());
// Remove old anchor.
tsdTitleEl.remove();
// Add `.no-children` class to `.tsd-parameters` that have no children. ----------------------------------------
// This is to allow removing styles from empty lists.
$('.tsd-parameters').each(function()
{
const parameterListEl = $(this);
if (parameterListEl.children().length === 0) { parameterListEl.addClass('no-children'); }
});
// Remove errant `tabindex` / `role` from index details summary header -----------------------------------------
// The details summary element is focusable!
$('details summary h5').attr('tabindex', null).attr('role', null);
// Augment scroll containers making them programmatically focusable --------------------------------------------
// Main container
$('div.container.container-main').attr('tabindex', -1);
// On This Page / Inner Element
$('details.tsd-page-navigation .tsd-accordion-details').attr('tabindex', -1);
// Sidebar container (used when in mobile mode)
$('div.col-sidebar').attr('tabindex', -1);
// Sidebar site menu container.
$('div.site-menu').attr('tabindex', -1);
// Breadcrumb modifications ------------------------------------------------------------------------------------
const breadcrumbListElements = $('.tsd-breadcrumb li');
const breadcrumbArray = breadcrumbListElements.toArray();
if (breadcrumbArray.length)
{
const firstElement = $(breadcrumbArray[0]);
if (firstElement.text() === NavigationIndex.projectName)
{
firstElement.remove();
breadcrumbArray.shift();
}
}
if (breadcrumbArray.length && NavigationIndex.hasMarkdown)
{
const firstElement = $(breadcrumbArray[0]);
if (firstElement.text() === NavigationIndex.markdownIndexName)
{
firstElement.remove();
breadcrumbArray.shift();
}
}
// There is only one link level left, so remove all links.
if (breadcrumbArray.length === 1) { breadcrumbListElements.remove(); }
}
/**
* Modifications for every page based on DMT options.
*
* @param {import('cheerio').Cheerio} $ -
*/
#augmentGlobalOptions($)
{
// Remove default theme navigation index content that isn't a module reflection. -------------------------------
// This is what is displayed when Javascript is disabled. Presently the default theme will render the first
// 20 reflections regardless of type. This can lead to bloat for large documentation efforts. Only displaying
// module reflections allows more precise navigation when Javascript is disabled.
$('nav.tsd-navigation #tsd-nav-container li').each(function()
{
const liEl = $(this);
const isModule = liEl.find('svg.tsd-kind-icon use[href="#icon-2"]');
if (!isModule.length) { liEl.remove(); }
});
}
/**
* Modifications for project reflection / `modules.html`.
*
* @param {import('cheerio').Cheerio} $ -
*/
#augmentProjectModules($)
{
// Find and replace all headers with text 'Modules'
$('summary.tsd-accordion-summary').each((_, element) =>
{
const summaryEl = $(element);
const h2El = summaryEl.find('h2');
// Optionally replace `Modules` for `Packages` in header.
if (this.#options.moduleRemap.isPackage)
{
// Replace the text node in the `On This Page` details / summary element.
summaryEl.contents().each((_, el) =>
{
if (el.type === 'text' && $(el).text().trim() === 'Modules') { $(el).replaceWith('Packages'); }
});
// Replace the text node in main details accordion inside `h2`.
h2El.contents().each((_, el) =>
{
if (el.type === 'text' && $(el).text().trim() === 'Modules') { $(el).replaceWith('Packages'); }
});
}
});
// Optionally remove module SVG.
if (!this.#options.navigation.moduleIcon)
{
$('svg').has('use[href$="#icon-2"]').remove();
}
}
/**
* Modifications for module reflection. Adds additional README that may be associated with a module / package
* particularly in a mono-repo use case facilitated by `typedoc-pkg`.
*
* @param {import('cheerio').Cheerio} $ -
*
* @param {PageEvent} page -
*/
#augmentModule($, page)
{
const moduleName = page.model?.name;
if (this.#options.moduleRemap.isPackage)
{
const titleEl = $('.tsd-page-title h1');
const titleText = titleEl.text();
if (typeof titleText === 'string') { titleEl.text(titleText.replace(/^Module (.*)/, 'Package $1')); }
}
if (typeof this.#options.moduleRemap.readme[moduleName] === 'string')
{
try
{
const md = fs.readFileSync(this.#options.moduleRemap.readme[moduleName], 'utf-8');
const mdHTML = this.#app.renderer.theme.markedPlugin.parseMarkdown(md, page);
if (typeof mdHTML === 'string') { $('.col-content').append(mdHTML); }
}
catch (err)
{
this.#app.logger.warn(
`[typedoc-theme-default-modern] Could not render additional 'README.md' for: '${moduleName}'`);
}
}
}
/**
* Modifications for class and interface reflections. Wraps `implements` and `hierarchy` panels in a details element.
*
* @param {import('cheerio').Cheerio} $ -
*/
#augmentWrapClassInterface($)
{
// Retrieve both section panels to potentially be modified -----------------------------------------------------
// To find any class hierarchy panel to modify alas it doesn't have a specific CSS class / identifier so we must
// do a text comparison searching for `h4` with text that starts with `Hierarchy`.
// TODO: Note that this may not work with any other language translations which is new to TypeDoc (0.26.x+).
const hierarchyPanelEl = $('section.tsd-panel').filter(function()
{
return $(this).find('h4').text().trim().startsWith('Hierarchy');
});
// To find any class implements panel to modify alas it doesn't have a specific CSS class / identifier so we must
// do a text comparison searching for `h4` with text that starts with `Implement`. This will find `Implements` on
// class pages and `Implemented by` on interface pages.
// TODO: Note that this may not work with any other language translations which is new to TypeDoc (0.26.x+).
const implementsPanelEl = $('section.tsd-panel').filter(function()
{
return $(this).find('h4').text().trim().startsWith('Implement');
});
// Enclose any class implements panel in a details / summary element. ------------------------------------------
if (implementsPanelEl.length)
{
const implementsHeaderEl = implementsPanelEl.find('h4');
const implementsText = implementsHeaderEl.text();
implementsHeaderEl.remove();
const childrenEl = implementsPanelEl.children();
const detailsEl = $(
`<section class="tsd-panel-group tsd-hierarchy">
<details class="tsd-hierarchy tsd-accordion">
<summary class="tsd-accordion-summary" data-key="reflection-implements">
<h5>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><use href="#icon-chevronSmall"></use></svg> ${implementsText}
</h5>
</summary>
<div class="tsd-accordion-details">
</div>
</details>
</section>`);
detailsEl.find('.tsd-accordion-details').append(childrenEl.clone());
childrenEl.remove();
if (hierarchyPanelEl.length)
{
// In the case that there is a hierarchy panel insert the implements panel before it.
detailsEl.insertBefore(hierarchyPanelEl);
implementsPanelEl.remove();
}
else
{
// Otherwise just replace the original implements panel.
implementsPanelEl.replaceWith(detailsEl);
}
}
// Enclose any class hierarchy panel in a details / summary element. -------------------------------------------
if (hierarchyPanelEl.length)
{
const hierarchyHeaderEl = hierarchyPanelEl.find('> h4');
const hierarchyContentEl = hierarchyPanelEl.find('> ul.tsd-hierarchy');
// Process header removing `view full` link and placing it next to the class target. ------------------------
const headerTextNodes = hierarchyHeaderEl.contents().filter(function() { return this.type === 'text'; });
const firstTextNode = headerTextNodes.first();
// Replace the original text with the updated text removing spaces and parentheses.
firstTextNode.replaceWith(firstTextNode.text().replace(/[ ()]/g, ''));
// Remove last parentheses.
headerTextNodes.last()?.remove();
// Move link to `target` class span.
const viewFullLink = hierarchyHeaderEl.find('a');
const targetClassSpan = hierarchyContentEl.find('span.tsd-hierarchy-target');
if (viewFullLink.length)
{
targetClassSpan.append(' (');
targetClassSpan.append(viewFullLink.clone());
targetClassSpan.append(')');
viewFullLink.remove();
}
// Create details element wrapper and update content --------------------------------------------------------
const detailsEl = $(
`<section class="tsd-panel-group tsd-hierarchy">
<details class="tsd-hierarchy tsd-accordion">
<summary class="tsd-accordion-summary" data-key="inheritance-hierarchy">
<h5>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><use href="#icon-chevronSmall"></use></svg>
</h5>
</summary>
<div class="tsd-accordion-details">
</div>
</details>
</section>`);
// Append content.
detailsEl.find('.tsd-accordion-details').append(hierarchyContentEl.clone());
const detailsH5El = detailsEl.find('h5');
// Append the HTML from old header.
detailsH5El.append(` ${hierarchyHeaderEl.html()}`);
hierarchyPanelEl.replaceWith(detailsEl);
}
}
/**
* Modifications for module and namespace reflections. Wraps `.tsd-index-panel` in a details element if missing.
*
* @param {import('cheerio').Cheerio} $ -
*/
#augmentWrapIndexDetails($)
{
const indexPanelEl = $('.tsd-panel.tsd-index-panel');
// Enclose module index in a details / summary element if the first child is not already a details element.
if (indexPanelEl && indexPanelEl?.children()?.first()?.get(0)?.tagName !== 'details')
{
indexPanelEl.find('h3.tsd-index-heading.uppercase').first().remove();
const childrenEl = indexPanelEl.children();
const detailsEl = $(
`<details class="tsd-index-content tsd-accordion dmt-index-content">
<summary class="tsd-accordion-summary tsd-index-summary" data-key="index">
<h3 class="tsd-index-heading uppercase">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><use href="#icon-chevronSmall"></use></svg> Index
</h3>
</summary>
<div class="tsd-accordion-details">
</div>
</details>`);
detailsEl.find('.tsd-accordion-details').append(childrenEl.clone());
childrenEl.remove();
indexPanelEl.append(detailsEl);
}
}
/**
* @param {PageEvent} page -
*/
#handlePageEnd(page)
{
const $ = load$1(page.contents);
// Append scripts to load web components.
this.#addAssets($, page);
// Remove unused assets / scripts from TypeDoc default theme.
this.#removeAssets($);
switch (page.model.kind)
{
case ReflectionKind.Class:
this.#augmentWrapClassInterface($);
break;
case ReflectionKind.Interface:
this.#augmentWrapClassInterface($);
break;
case ReflectionKind.Module:
this.#augmentWrapIndexDetails($);
this.#augmentModule($, page);
break;
case ReflectionKind.Namespace:
this.#augmentWrapIndexDetails($);
break;
case ReflectionKind.Project:
if (page.url.endsWith('modules.html'))
{
this.#augmentProjectModules($);
}
break;
}
// A few global modifications tweaks like the favicon and slight modifications to the layout to allow right
// aligning of additional elements in flexbox layouts.
this.#augmentGlobal($);
// Further global modifications based on DMT options.
this.#augmentGlobalOptions($);
page.contents = $.html();
}
/**
* Remove unused assets / scripts from TypeDoc default theme.
*
* @param {import('cheerio').Cheerio} $ -
*/
#removeAssets($)
{
// Remove the default theme navigation script.
$('script[src$="assets/navigation.js"]').remove();
// Remove unused default theme assets.
$('script[src$="assets/search.js"]').remove();
}
}
var decoder;
try {
decoder = new TextDecoder();
} catch(error) {}
var src;
var srcEnd;
var position$1 = 0;
var currentUnpackr = {};
var currentStructures;
var srcString;
var srcStringStart = 0;
var srcStringEnd = 0;
var bundledStrings$1;
var referenceMap;
var currentExtensions = [];
var dataView;
var defaultOptions = {
useRecords: false,
mapsAsObjects: true
};
class C1Type {}
const C1 = new C1Type();
C1.name = 'MessagePack 0xC1';
var sequentialMode = false;
var inlineObjectReadThreshold = 2;
var readStruct;
// no-eval build
try {
new Function('');
} catch(error) {
// if eval variants are not supported, do not create inline object readers ever
inlineObjectReadThreshold = Infinity;
}
class Unpackr {
constructor(options) {
if (options) {
if (options.useRecords === false && options.mapsAsObjects === undefined)
options.mapsAsObjects = true;
if (options.sequential && options.trusted !== false) {
options.trusted = true;
if (!options.structures && options.useRecords != false) {
options.structures = [];
if (!options.maxSharedStructures)
options.maxSharedStructures = 0;
}
}
if (options.structures)
options.structures.sharedLength = options.structures.length;
else if (options.getStructures) {
(options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
options.structures.sharedLength = 0;
}
if (options.int64AsNumber) {
options.int64AsType = 'number';
}
}
Object.assign(this, options);
}
unpack(source, options) {
if (src) {
// re-entrant execution, save the state and restore it after we do this unpack
return saveState(() => {
clearSource();
return this ? this.unpack(source, options) : Unpackr.prototype.unpack.call(defaultOptions, source, options)
})
}
if (!source.buffer && source.constructor === ArrayBuffer)
source = typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
if (typeof options === 'object') {
srcEnd = options.end || source.length;
position$1 = options.start || 0;
} else {
position$1 = 0;
srcEnd = options > -1 ? options : source.length;
}
srcStringEnd = 0;
srcString = null;
bundledStrings$1 = null;
src = source;
// this provides cached access to the data view for a buffer if it is getting reused, which is a recommend
// technique for getting data from a database where it can be copied into an existing buffer instead of creating
// new ones
try {
dataView = source.dataView || (source.dataView = new DataView(source.buffer, source.byteOffset, source.byteLength));
} catch(error) {
// if it doesn't have a buffer, maybe it is the wrong type of object
src = null;
if (source instanceof Uint8Array)
throw error
throw new Error('Source must be a Uint8Array or Buffer but was a ' + ((source && typeof source == 'object') ? source.constructor.name : typeof source))
}
if (this instanceof Unpackr) {
currentUnpackr = this;
if (this.structures) {
currentStructures = this.structures;
return checkedRead(options)
} else if (!currentStructures || currentStructures.length > 0) {
currentStructures = [];
}
} else {
currentUnpackr = defaultOptions;
if (!currentStructures || currentStructures.length > 0)
currentStructures = [];
}
return checkedRead(options)
}
unpackMultiple(source, forEach) {
let values, lastPosition = 0;
try {
sequentialMode = true;
let size = source.length;
let value = this ? this.unpack(source, size) : defaultUnpackr.unpack(source, size);
if (forEach) {
if (forEach(value, lastPosition, position$1) === false) return;
while(position$1 < size) {
lastPosition = position$1;
if (forEach(checkedRead(), lastPosition, position$1) === false) {
return
}
}
}
else {
values = [ value ];
while(position$1 < size) {
lastPosition = position$1;
values.push(checkedRead());
}
return values
}
} catch(error) {
error.lastPosition = lastPosition;
error.values = values;
throw error
} finally {
sequentialMode = false;
clearSource();
}
}
_mergeStructures(loadedStructures, existingStructures) {
loadedStructures = loadedStructures || [];
if (Object.isFrozen(loadedStructures))
loadedStructures = loadedStructures.map(structure => structure.slice(0));
for (let i = 0, l = loadedStructures.length; i < l; i++) {
let structure = loadedStructures[i];
if (structure) {
structure.isShared = true;
if (i >= 32)
structure.highByte = (i - 32) >> 5;
}
}
loadedStructures.sharedLength = loadedStructures.length;
for (let id in existingStructures || []) {
if (id >= 0) {
let structure = loadedStructures[id];
let existing = existingStructures[id];
if (existing) {
if (structure)
(loadedStructures.restoreStructures || (loadedStructures.restoreStructures = []))[id] = structure;
loadedStructures[id] = existing;
}
}
}
return this.structures = loadedStructures
}
decode(source, options) {
return this.unpack(source, options)
}
}
function checkedRead(options) {
try {
if (!currentUnpackr.trusted && !sequentialMode) {
let sharedLength = currentStructures.sharedLength || 0;
if (sharedLength <