UNPKG

@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
/** * @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 <