docs: create typedoc theme and plugins for references (#5297)

* update typedoc and its plugins

* refactor existing typedoc configurations

* added new typedoc plugin and themes

* added more customization options

* added more customization options

* refactored doc-utils to a workspace

* fix tsconfig

* update README files

* remove comments

* revert type changes

* remove dependencies no longer needed

* removed modules action
This commit is contained in:
Shahed Nasser
2023-10-05 12:09:42 +03:00
committed by GitHub
parent b3f75d8f21
commit 0350eeb0a1
110 changed files with 7182 additions and 930 deletions
@@ -0,0 +1,90 @@
#!/usr/bin/env node
import { Octokit } from "@octokit/core"
import fs from "fs"
import path from "path"
const shouldExpire = process.argv.indexOf("--expire") !== -1
const octokit = new Octokit({
auth: process.env.GH_TOKEN,
})
async function main() {
let announcement = {}
if (shouldExpire) {
//check if the file was last updated 6 days ago
try {
const commitResponse = await octokit.request(
"GET /repos/{owner}/{repo}/commits",
{
owner: "medusajs",
repo: "medusa",
path: path.join("www", "apps", "docs", "announcement.json"),
per_page: 1,
}
)
if (
commitResponse.data.length &&
commitResponse.data[0].commit.committer?.date &&
dateDiffInDays(
new Date(commitResponse.data[0].commit.committer.date),
new Date()
) < 6
) {
console.log("File was edited less than 6 days ago. Expiry canceled.")
return
}
} catch (e) {
//continue as if file doesn't exist
}
} else {
//retrieve the latest release
const response = await octokit.request(
"GET /repos/{owner}/{repo}/releases/latest",
{
owner: "medusajs",
repo: "medusa",
}
)
const version = response.data.tag_name
//add new announcement
announcement = {
id: response.data.html_url,
content: `${version} is out`,
isCloseable: true,
}
}
//write new config file
fs.writeFileSync(
path.join(
__dirname,
"..",
"..",
"..",
"www",
"apps",
"docs",
"announcement.json"
),
JSON.stringify(announcement)
)
console.log(`Announcement Bar has been ${shouldExpire ? "removed" : "added"}`)
}
const _MS_PER_DAY = 1000 * 60 * 60 * 24
// a and b are javascript Date objects
function dateDiffInDays(a: Date, b: Date) {
// Discard the time and time-zone information.
const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate())
const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate())
return Math.floor((utc2 - utc1) / _MS_PER_DAY)
}
void main()
@@ -0,0 +1,171 @@
#!/usr/bin/env node
import { LinearClient } from "@linear/sdk"
import { Octokit } from "@octokit/core"
import fs from "fs"
import path from "path"
const octokit = new Octokit({
auth: process.env.GH_TOKEN,
})
const linearClient = new LinearClient({
apiKey: process.env.LINEAR_API_KEY,
})
const repoPath = path.join(
__dirname,
"..",
"..",
"..",
"www",
"apps",
"docs",
"content"
)
let freshnessCheckLabelId = ""
let documentationTeamId = ""
async function scanDirectory(startPath: string) {
const files = fs.readdirSync(path.join(startPath), {
withFileTypes: true,
})
for (const file of files) {
const filePath = path.join(startPath, file.name)
if (file.isDirectory()) {
//if it's references directory, skip
if (file.name !== "references" && file.name !== "upgrade-guides") {
await scanDirectory(filePath)
}
continue
}
//check that the file is a markdown file
if (file.name.indexOf(".md") === -1 && file.name.indexOf(".mdx") === -1) {
continue
}
//if it is a file, check its commits in GitHub
const commitResponse = await octokit.request(
"GET /repos/{owner}/{repo}/commits",
{
owner: "medusajs",
repo: "medusa",
path: filePath,
per_page: 1,
}
)
if (
!commitResponse.data.length ||
!commitResponse.data[0].commit.committer?.date
) {
continue
}
const today = new Date()
const lastEditedDate = new Date(
commitResponse.data[0].commit.committer.date
)
const monthsSinceEdited = getMonthDifference(lastEditedDate, today)
if (monthsSinceEdited > 6) {
//file was edited more than 6 months ago.
//check if there's an issue created for this file since the commit date
const existingIssue = await linearClient.issues({
filter: {
createdAt: {
gte: subtractMonths(monthsSinceEdited - 6, today),
},
title: {
containsIgnoreCase: `Freshness check for ${filePath}`,
},
labels: {
some: {
id: {
eq: freshnessCheckLabelId,
},
},
},
},
first: 1,
})
if (existingIssue.nodes.length) {
//an issue has been created for the past 6 months. Don't create an issue for it.
continue
}
console.log(`Creating an issue for ${filePath}...`)
//there are no issues in the past 6 months. Create an issue
await linearClient.issueCreate({
teamId: documentationTeamId,
title: `Freshness check for ${filePath}`,
labelIds: [freshnessCheckLabelId],
description: `File \`${filePath}\` was last edited on ${lastEditedDate.toDateString()}.`,
})
}
}
}
async function main() {
//fetch documentation team ID from linear
const documentationTeam = await linearClient.teams({
filter: {
name: {
eqIgnoreCase: "Documentation",
},
},
first: 1,
})
if (!documentationTeam.nodes.length) {
console.log("Please add Documentation team in Linear first then try again")
process.exit(1)
}
documentationTeamId = documentationTeam.nodes[0].id
//fetch freshness check label ID from linear
const freshnessCheckLabel = await linearClient.issueLabels({
filter: {
name: {
eqIgnoreCase: "type: freshness-check",
},
team: {
id: {
eq: documentationTeamId,
},
},
},
})
if (!freshnessCheckLabel.nodes.length) {
console.log(
"Please add freshness check label in Linear under the documentation team first then try again"
)
process.exit(1)
}
freshnessCheckLabelId = freshnessCheckLabel.nodes[0].id
await scanDirectory(repoPath)
}
function getMonthDifference(startDate: Date, endDate: Date) {
return (
endDate.getMonth() -
startDate.getMonth() +
12 * (endDate.getFullYear() - startDate.getFullYear())
)
}
function subtractMonths(numOfMonths: number, date = new Date()) {
date.setMonth(date.getMonth() - numOfMonths)
return date
}
void main()
@@ -0,0 +1,25 @@
#!/usr/bin/env node
import path from "path"
import fs from "fs"
import { exec } from "child_process"
const referenceNames = process.argv.slice(2)
referenceNames.forEach((names) => {
const configPathName = path.join(
__dirname,
"..",
"typedoc-config",
`${names}.js`
)
// check if the config file exists
if (!fs.existsSync(configPathName)) {
throw new Error(
`Config file for ${names} doesn't exist. Make sure to create it in ${configPathName}`
)
}
const typedocProcess = exec(`typedoc --options ${configPathName}`)
typedocProcess.stdout?.pipe(process.stdout)
typedocProcess.stderr?.pipe(process.stdout)
})
+28
View File
@@ -0,0 +1,28 @@
{
"name": "scripts",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"scripts": {
"generate:announcement": "ts-node ./doc-change-release.ts",
"generate:reference": "ts-node ./generate-reference.ts",
"check:freshness": "ts-node ./freshness-check.ts"
},
"version": "0.0.0",
"dependencies": {
"@linear/sdk": "^1.22.0",
"@octokit/core": "^4.0.5",
"ts-node": "^10.9.1",
"typedoc": "0.25.1",
"typedoc-monorepo-link-types": "^0.0.2",
"typedoc-plugin-frontmatter": "*",
"typedoc-plugin-markdown": "3.16.0",
"typedoc-plugin-markdown-medusa": "*",
"typedoc-plugin-merge-modules": "5.1.0",
"typedoc-plugin-modules": "*",
"typedoc-plugin-reference-excluder": "1.1.3",
"typedoc-plugin-rename-defaults": "^0.6.6"
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["*.ts"]
}
@@ -0,0 +1,4 @@
module.exports = {
plugin: ["typedoc-plugin-markdown"],
readme: "none",
}
@@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path")
const globalTypedocOptions = require("./typedoc")
const pathPrefix = path.join(__dirname, "..", "..")
module.exports = {
...globalTypedocOptions,
entryPoints: [path.join(pathPrefix, "packages/medusa/src/models/index.ts")],
out: [path.join(pathPrefix, "www/apps/docs/content/references/entities")],
tsconfig: path.join(pathPrefix, "packages/medusa/tsconfig.json"),
name: "Entities Reference",
indexTitle: "Entities Reference",
entryDocument: "index.md",
hideInPageTOC: true,
hideBreadcrumbs: true,
plugin: [...globalTypedocOptions.plugin, "typedoc-plugin-frontmatter"],
frontmatterData: {
displayed_sidebar: "entitiesSidebar",
},
}
@@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path")
const globalTypedocOptions = require("./typedoc")
const pathPrefix = path.join(__dirname, "..", "..")
module.exports = {
...globalTypedocOptions,
entryPoints: [path.join(pathPrefix, "packages/medusa-js/src/resources")],
entryPointStrategy: "expand",
out: [path.join(pathPrefix, "www/apps/docs/content/references/js-client")],
tsconfig: path.join(pathPrefix, "packages/medusa-js/tsconfig.json"),
name: "JS Client Reference",
indexTitle: "JS Client Reference",
entryDocument: "index.md",
hideInPageTOC: true,
hideBreadcrumbs: true,
plugin: [
...globalTypedocOptions.plugin,
"typedoc-plugin-merge-modules",
"typedoc-plugin-reference-excluder",
"typedoc-plugin-frontmatter",
],
exclude: [
path.join(pathPrefix, "packages/medusa-js/src/resources/base.ts"),
"node_modules/**",
"packages/**/node_modules",
],
excludeConstructors: true,
frontmatterData: {
displayed_sidebar: "jsClientSidebar",
},
pagesPattern: "internal\\.",
}
@@ -0,0 +1,71 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path")
const globalTypedocOptions = require("./base")
const pathPrefix = path.join(__dirname, "..", "..", "..")
module.exports = ({
entryPointPath,
outPath,
tsconfigPath = "",
moduleName = "",
documentsToFormat = [],
additionalFormatting = {},
extraOptions = {},
}) => {
const formatting = {}
documentsToFormat.forEach((document) => {
formatting[document] = {
reflectionTitle: {
kind: false,
typeParameters: false,
suffix: "Reference",
},
expandMembers: true,
showCommentsAsHeader: true,
parameterStyle: "list",
useTsLinkResolution: false,
showReturnSignature: false,
sections: {
reflection_typeParameters: false,
member_declaration_typeParameters: false,
reflection_implements: false,
reflection_implementedBy: false,
reflection_callable: false,
reflection_indexable: false,
member_signature_typeParameters: false,
member_signature_sources: false,
member_signature_title: false,
},
reflectionGroups: {
Constructors: false,
Properties: false,
},
...additionalFormatting,
}
})
return {
...globalTypedocOptions,
entryPoints: [path.join(pathPrefix, entryPointPath)],
out: [path.join(pathPrefix, outPath)],
tsconfig: path.join(
pathPrefix,
tsconfigPath || "packages/types/tsconfig.json"
),
name: `${moduleName} Reference`,
indexTitle: `${moduleName} Reference`,
entryDocument: "index.md",
entryPointStrategy: "expand",
hideInPageTOC: true,
hideBreadcrumbs: true,
plugin: [
"typedoc-plugin-markdown-medusa",
"typedoc-plugin-modules",
"typedoc-plugin-rename-defaults",
"typedoc-plugin-frontmatter",
],
hideMembersSymbol: true,
formatting,
...extraOptions,
}
}
@@ -0,0 +1,9 @@
{
"name": "typedoc-config",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"version": "0.0.0"
}
@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path")
const globalTypedocOptions = require("./typedoc")
const pathPrefix = path.join(__dirname, "..", "..")
module.exports = {
...globalTypedocOptions,
entryPoints: [path.join(pathPrefix, "packages/medusa/src/services/index.ts")],
out: [path.join(pathPrefix, "www/apps/docs/content/references/services")],
tsconfig: path.join(pathPrefix, "packages/medusa/tsconfig.json"),
name: "Services Reference",
indexTitle: "Services Reference",
entryDocument: "index.md",
hideInPageTOC: true,
hideBreadcrumbs: true,
}
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["*.js"]
}
@@ -0,0 +1,2 @@
dist
.yarn
@@ -0,0 +1,43 @@
# typedoc-plugin-frontmatter
A Typedoc plugin that allows inserting frontmatter key-value pairs at the top of the exported Markdown files.
## Configurations
The following options are optional and can be used to customize the configurations of the plugin.
### frontmatterData
`frontmatterData` is an object of key-value pairs. If none provided, no frontmatter variables will be added to the Markdown files.
An example of passing it in a JavaScript configuration file:
```js
frontmatterData: {
displayed_sidebar: "jsClientSidebar",
},
```
If passing the option in the command line the value should be a JSON object.
### pagesPattern
`pagesPattern` is a string that contains a regular expression. This allows you to limit the pages the frontmatter variables should be added to.
By default, the frontmatter variables will be added to all files.
An example of passing it in a JavaScript configuration file:
```js
frontmatterData: {
pagesPattern: "internal\\.",
},
```
## Build the Plugin
Before using any command that makes use of this plugin, make sure to run the `build` command:
```bash
yarn build
```
@@ -0,0 +1,35 @@
{
"name": "typedoc-plugin-frontmatter",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"version": "0.0.0",
"description": "Plugin to add frontmatter key-values at the top of pages",
"main": "./dist/index.js",
"exports": "./dist/index.js",
"files": [
"dist"
],
"author": "Shahed Nasser",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"lint": "eslint --ext .ts src"
},
"peerDependencies": {
"typedoc": "0.25.x"
},
"devDependencies": {
"@types/node": "^16.11.10",
"typedoc": "^0.25.1",
"typescript": "^4.6"
},
"keywords": [
"typedocplugin",
"packages",
"monorepo",
"typedoc"
]
}
@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Application, PageEvent, ParameterType } from "typedoc"
export function load(app: Application) {
app.options.addDeclaration({
name: "frontmatterData",
help: "An object of key-value pairs to be added to frontmatter",
type: ParameterType.Mixed, // The default
defaultValue: {},
validate: (value: any) => {
if (typeof value === "string") {
//decode it with JSON to check if it's an object
value = JSON.parse(value)
}
if (
!(typeof value === "object" && !Array.isArray(value) && value !== null)
) {
throw new Error("Value should be an object")
}
},
})
app.options.addDeclaration({
name: "pagesPattern",
help: "A string of pages pattern. The pattern will be tested using RegExp to determine whether the frontmatterData will be added or not.",
type: ParameterType.String,
defaultValue: "",
})
app.renderer.on(PageEvent.END, (page: PageEvent) => {
const patternStr: string | any = app.options.getValue("pagesPattern")
const pattern = new RegExp(patternStr)
let frontmatterData: object | any = app.options.getValue("frontmatterData")
if (typeof frontmatterData === "string") {
frontmatterData = JSON.parse(frontmatterData)
}
const frontmatterDataEntries = Object.entries(frontmatterData)
if (!frontmatterDataEntries.length || !pattern.test(page.filename)) {
return
}
let frontmatterStr = `---\n`
for (const [key, value] of frontmatterDataEntries) {
frontmatterStr += `${key}: ${value}\n`
}
frontmatterStr += `---\n\n`
page.contents = frontmatterStr + page.contents
})
}
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src"]
}
@@ -0,0 +1,2 @@
dist
.yarn
@@ -0,0 +1,28 @@
# typedoc-plugin-markdown-medusa
A plugin that forks and customizes the [typedoc-plugin-markdown](https://github.com/tgreyuk/typedoc-plugin-markdown/tree/master/packages/typedoc-plugin-markdown) to create a theme with more formatting options.
## Configurations
Aside from the options detailed in [typedoc-plugin-markdown](https://github.com/tgreyuk/typedoc-plugin-markdown/tree/master/packages/typedoc-plugin-markdown#options), the following options are accepted:
- `formatting`: An object whose keys are string patterns used to target specific files. You can also use the string `*` to target all files. The values are objects having the following properties:
- `sections`: (optional) an object whose keys are of type [`SectionKey`](./src/types.ts#L19) and values are boolean. This property is used to enable/disable certain sections in the outputted generated docs.
- `reflectionGroups`: (optional) an object whose keys are titles of reflection groups (for example, `Constructors`), and values are boolean. This property is used to enable/disable reflection group from being documented.
- `reflectionTitle`: (optional) an object used to customize how the title of a generated documentation page is rendered. It accepts the following properties:
- `kind`: a boolean value indicating whether the documented resource's kind should be shown in the title.
- `typeParameters`: a boolean value indicating whether the documented resource's type parameters should b shown in the title.
- `suffix`: a string used to add additional text to the end of the page's title.
- `reflectionDescription`: (optional) a string used to add description in a documentation page after the page's title.
- `expandMembers`: (optional) a boolean indicating whether members in a page should be expanded. When enabled, the member titles (for example, `Methods`) are removed and the heading level of nested titles whithin the member is elevated by `1`.
- `showCommentAsHeader`: (optional) a boolean indicating whether comments, for example, a method's name, are represented as headers.
- `parameterStyle`: (optional) a string indicating how parameters are displayed. Its value can be `table` (default) or `list`.
- `showReturnSignature`: (optional) a boolean indicating whether to show the signature for returned values.
## Build Plugin
Before using any command that makes use of this plugin, make sure to run the `build` command:
```bash
yarn build
```
@@ -0,0 +1,37 @@
{
"name": "typedoc-plugin-markdown-medusa",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"version": "1.0.0",
"description": "A plugin that creates a theme with customization capabilities.",
"main": "./dist/index.js",
"exports": "./dist/index.js",
"files": [
"dist"
],
"author": "Shahed Nasser",
"scripts": {
"build": "tsc && copyfiles --up 1 ./src/**/*.hbs ./dist/",
"lint": "eslint --ext .ts src"
},
"peerDependencies": {
"typedoc": "0.25.x"
},
"devDependencies": {
"@types/node": "^16.11.10",
"copyfiles": "^2.4.1",
"typescript": "^4.6"
},
"keywords": [
"typedocplugin",
"packages",
"monorepo",
"typedoc"
],
"dependencies": {
"handlebars": "^4.7.8"
}
}
@@ -0,0 +1,23 @@
import { ReflectionKind } from "typedoc"
const PLURALS = {
[ReflectionKind.Class]: "Classes",
[ReflectionKind.Property]: "Properties",
[ReflectionKind.Enum]: "Enumerations",
[ReflectionKind.EnumMember]: "Enumeration members",
[ReflectionKind.TypeAlias]: "Type aliases",
}
export function getKindPlural(kind: ReflectionKind): string {
if (kind in PLURALS) {
return PLURALS[kind as keyof typeof PLURALS]
} else {
return getKindString(kind) + "s"
}
}
function getKindString(kind: ReflectionKind): string {
let str = ReflectionKind[kind]
str = str.replace(/(.)([A-Z])/g, (_m, a, b) => a + " " + b.toLowerCase())
return str
}
@@ -0,0 +1,106 @@
import { Application, ParameterType } from "typedoc"
import { MarkdownThemeOptionsReader } from "./options-reader"
import { MarkdownTheme } from "./theme"
export function load(app: Application) {
app.renderer.defineTheme("markdown", MarkdownTheme)
app.options.addReader(new MarkdownThemeOptionsReader())
app.options.addDeclaration({
help: "[Markdown Plugin] Do not render page title.",
name: "hidePageTitle",
type: ParameterType.Boolean,
defaultValue: false,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Do not render breadcrumbs in template.",
name: "hideBreadcrumbs",
type: ParameterType.Boolean,
defaultValue: false,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Specifies the base path that all links to be served from. If omitted all urls will be relative.",
name: "publicPath",
type: ParameterType.String,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Use HTML named anchors as fragment identifiers for engines that do not automatically assign header ids. Should be set for Bitbucket Server docs.",
name: "namedAnchors",
type: ParameterType.Boolean,
defaultValue: false,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Output all reflections into seperate output files.",
name: "allReflectionsHaveOwnDocument",
type: ParameterType.Boolean,
defaultValue: false,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Separator used to format filenames.",
name: "filenameSeparator",
type: ParameterType.String,
defaultValue: ".",
})
app.options.addDeclaration({
help: "[Markdown Plugin] The file name of the entry document.",
name: "entryDocument",
type: ParameterType.String,
defaultValue: "README.md",
})
app.options.addDeclaration({
help: "[Markdown Plugin] Do not render in-page table of contents items.",
name: "hideInPageTOC",
type: ParameterType.Boolean,
defaultValue: false,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Customise the index page title.",
name: "indexTitle",
type: ParameterType.String,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Do not add special symbols for class members.",
name: "hideMembersSymbol",
type: ParameterType.Boolean,
defaultValue: false,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Preserve anchor casing when generating links.",
name: "preserveAnchorCasing",
type: ParameterType.Boolean,
defaultValue: false,
})
app.options.addDeclaration({
help: "[Markdown Plugin] Specify the Type Declaration Render Style",
name: "objectLiteralTypeDeclarationStyle",
type: ParameterType.String,
defaultValue: "table",
validate: (x) => {
const availableValues = ["table", "list"]
if (!availableValues.includes(x)) {
throw new Error(
`Wrong value for objectLiteralTypeDeclarationStyle, the expected value is one of ${availableValues}`
)
}
},
})
app.options.addDeclaration({
help: "[Markdown Plugin] Formatting options that can be specified either on a specific document or to all documents",
name: "formatting",
type: ParameterType.Object,
defaultValue: {},
})
}
export { MarkdownTheme }
@@ -0,0 +1,12 @@
import { Options, OptionsReader } from "typedoc"
export class MarkdownThemeOptionsReader implements OptionsReader {
name = "markdown-theme-reader"
readonly order = 1000
readonly supportsPackages = false
read(container: Options) {
if (container.getValue("theme") === "default") {
container.setValue("theme", "markdown")
}
}
}
@@ -0,0 +1,99 @@
import * as fs from "fs"
import * as Handlebars from "handlebars"
import * as path from "path"
import breadcrumbsHelper from "./resources/helpers/breadcrumbs"
import commentHelper from "./resources/helpers/comment"
import commentsHelper from "./resources/helpers/comments"
import declarationTitleHelper from "./resources/helpers/declaration-title"
import escapeHelper from "./resources/helpers/escape"
import hierarchyHelper from "./resources/helpers/hierarchy"
import ifIsReference from "./resources/helpers/if-is-reference"
import ifNamedAnchors from "./resources/helpers/if-named-anchors"
import ifShowBreadcrumbsHelper from "./resources/helpers/if-show-breadcrumbs"
import ifShowNamedAnchorsHelper from "./resources/helpers/if-show-named-anchors"
import ifShowPageTitleHelper from "./resources/helpers/if-show-page-title"
import ifShowReturnsHelper from "./resources/helpers/if-show-returns"
import ifShowTypeHierarchyHelper from "./resources/helpers/if-show-type-hierarchy"
import indexSignatureTitleHelper from "./resources/helpers/index-signature-title"
import parameterTableHelper from "./resources/helpers/parameter-table"
import objectLiteralMemberHelper from "./resources/helpers/type-declaration-object-literal"
import referenceMember from "./resources/helpers/reference-member"
import reflectionPathHelper from "./resources/helpers/reflection-path"
import reflectionTitleHelper from "./resources/helpers/reflection-title"
import relativeUrlHelper from "./resources/helpers/relative-url"
import returns from "./resources/helpers/returns"
import signatureTitleHelper from "./resources/helpers/signature-title"
import tocHelper from "./resources/helpers/toc"
import typeHelper from "./resources/helpers/type"
import typeAndParentHelper from "./resources/helpers/type-and-parent"
import typeParameterTableHelper from "./resources/helpers/type-parameter-table"
import sectionsHelper from "./resources/helpers/section-enabled"
import getFormattingOptionHelper from "./resources/helpers/get-formatting-option"
import titleLevelHelper from "./resources/helpers/title-level"
import typeParameterListHelper from "./resources/helpers/type-parameter-list"
import typeParameterHelper from "./resources/helpers/type-parameter"
import parameterListHelper from "./resources/helpers/parameter-list"
import parameterHelper from "./resources/helpers/parameter"
import { MarkdownTheme } from "./theme"
const TEMPLATE_PATH = path.join(__dirname, "resources", "templates")
export const indexTemplate = Handlebars.compile(
fs.readFileSync(path.join(TEMPLATE_PATH, "index.hbs")).toString()
)
export const reflectionTemplate = Handlebars.compile(
fs.readFileSync(path.join(TEMPLATE_PATH, "reflection.hbs")).toString()
)
export const reflectionMemberTemplate = Handlebars.compile(
fs.readFileSync(path.join(TEMPLATE_PATH, "reflection.member.hbs")).toString()
)
export function registerPartials() {
const partialsFolder = path.join(__dirname, "resources", "partials")
const partialFiles = fs.readdirSync(partialsFolder)
partialFiles.forEach((partialFile) => {
const partialName = path.basename(partialFile, ".hbs")
const partialContent = fs
.readFileSync(partialsFolder + "/" + partialFile)
.toString()
Handlebars.registerPartial(partialName, partialContent)
})
}
export function registerHelpers(theme: MarkdownTheme) {
breadcrumbsHelper(theme)
commentHelper(theme)
commentsHelper(theme)
declarationTitleHelper(theme)
escapeHelper()
hierarchyHelper()
ifIsReference()
ifNamedAnchors(theme)
ifShowBreadcrumbsHelper(theme)
ifShowNamedAnchorsHelper(theme)
ifShowPageTitleHelper(theme)
ifShowReturnsHelper()
ifShowTypeHierarchyHelper()
indexSignatureTitleHelper()
parameterTableHelper()
objectLiteralMemberHelper(theme)
referenceMember()
reflectionPathHelper()
reflectionTitleHelper(theme)
relativeUrlHelper(theme)
returns()
signatureTitleHelper(theme)
tocHelper(theme)
typeHelper()
typeAndParentHelper()
typeParameterTableHelper()
sectionsHelper(theme)
getFormattingOptionHelper(theme)
titleLevelHelper(theme)
typeParameterListHelper()
typeParameterHelper(theme)
parameterListHelper()
parameterHelper(theme)
}
@@ -0,0 +1,51 @@
import * as Handlebars from "handlebars"
import { PageEvent } from "typedoc"
import { MarkdownTheme } from "../../theme"
import { escapeChars, getDisplayName } from "../../utils"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper("breadcrumbs", function (this: PageEvent) {
const { entryPoints, entryDocument, project, readme } = theme
if (!project) {
return ""
}
const hasReadmeFile = !readme.endsWith("none")
const breadcrumbs: string[] = []
const globalsName = entryPoints.length > 1 ? "Modules" : "Exports"
breadcrumbs.push(
this.url === entryDocument
? project.name
: `[${getDisplayName(project)}](${Handlebars.helpers.relativeURL(
entryDocument
)})`
)
if (hasReadmeFile) {
breadcrumbs.push(
this.url === project.url
? globalsName
: `[${globalsName}](${Handlebars.helpers.relativeURL("modules.md")})`
)
}
const breadcrumbsOut = breadcrumb(this, this.model, breadcrumbs)
return breadcrumbsOut
})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function breadcrumb(page: PageEvent, model: any, md: string[]) {
if (model && model.parent) {
breadcrumb(page, model.parent, md)
if (model.url) {
md.push(
page.url === model.url
? `${escapeChars(model.name)}`
: `[${escapeChars(model.name)}](${Handlebars.helpers.relativeURL(
model.url
)})`
)
}
}
return md.join(" / ")
}
@@ -0,0 +1,120 @@
import * as fs from "fs"
import * as Handlebars from "handlebars"
import * as Path from "path"
import { CommentDisplayPart } from "typedoc/dist/lib/models/comments/comment"
import { MarkdownTheme } from "../../theme"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper("comment", function (parts: CommentDisplayPart[]) {
const result: string[] = []
for (const part of parts) {
switch (part.kind) {
case "text":
case "code":
result.push(part.text)
break
case "inline-tag":
switch (part.tag) {
case "@label":
case "@inheritdoc":
break
case "@link":
case "@linkcode":
case "@linkplain": {
if (part.target) {
const url =
typeof part.target === "string"
? part.target
: "url" in part.target
? Handlebars.helpers.relativeURL(part.target.url)
: ""
const wrap = part.tag === "@linkcode" ? "`" : ""
result.push(
url ? `[${wrap}${part.text}${wrap}](${url})` : part.text
)
} else {
result.push(part.text)
}
break
}
default:
result.push(`{${part.tag} ${part.text}}`)
break
}
break
default:
result.push("")
}
}
return parseMarkdown(result.join(""), theme)
})
}
function parseMarkdown(text: string, theme: MarkdownTheme) {
const includePattern = /\[\[include:([^\]]+?)\]\]/g
const mediaPattern = /media:\/\/([^ ")\]}]+)/g
if (theme.includes) {
text = text.replace(includePattern, (_match, path) => {
path = Path.join(theme.includes!, path.trim())
if (fs.existsSync(path) && fs.statSync(path).isFile()) {
const contents = readFile(path)
return contents
} else {
theme.application.logger.warn("Could not find file to include: " + path)
return ""
}
})
}
if (theme.mediaDirectory) {
text = text.replace(mediaPattern, (match: string, path: string) => {
const fileName = Path.join(theme.mediaDirectory!, path)
if (fs.existsSync(fileName) && fs.statSync(fileName).isFile()) {
return Handlebars.helpers.relativeURL("media") + "/" + path
} else {
theme.application.logger.warn("Could not find media file: " + fileName)
return match
}
})
}
return text
}
/**
* Load the given file and return its contents.
*
* @param file The path of the file to read.
* @returns The files contents.
*/
export function readFile(file: string): string {
const buffer = fs.readFileSync(file)
switch (buffer[0]) {
case 0xfe:
if (buffer[1] === 0xff) {
let i = 0
while (i + 1 < buffer.length) {
const temp = buffer[i]
buffer[i] = buffer[i + 1]
buffer[i + 1] = temp
i += 2
}
return buffer.toString("ucs2", 2)
}
break
case 0xff:
if (buffer[1] === 0xfe) {
return buffer.toString("ucs2", 2)
}
break
case 0xef:
if (buffer[1] === 0xbb) {
return buffer.toString("utf8", 3)
}
}
return buffer.toString("utf8", 0)
}
@@ -0,0 +1,42 @@
import * as Handlebars from "handlebars"
import { Comment } from "typedoc"
import { camelToTitleCase } from "../../utils"
import { MarkdownTheme } from "../../theme"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"comments",
function (
comment: Comment,
showSummary = true,
showTags = true,
commentLevel = 4
) {
const { showCommentsAsHeader } = theme.getFormattingOptionsForLocation()
const md: string[] = []
if (showSummary && comment.summary) {
md.push(Handlebars.helpers.comment(comment.summary))
}
const filteredTags = comment.blockTags.filter(
(tag) => tag.tag !== "@returns"
)
if (showTags && comment.blockTags?.length) {
const tags = filteredTags.map((tag) => {
return `${
showCommentsAsHeader
? `${Handlebars.helpers.titleLevel(commentLevel)} `
: "**`"
}${camelToTitleCase(tag.tag.substring(1))}${
showCommentsAsHeader ? "" : "`**"
}\n\n${Handlebars.helpers.comment(tag.content)}`
})
md.push(tags.join("\n\n"))
}
return md.join("\n\n")
}
)
}
@@ -0,0 +1,65 @@
import * as Handlebars from "handlebars"
import {
DeclarationReflection,
LiteralType,
ParameterReflection,
ReflectionKind,
ReflectionType,
} from "typedoc"
import { MarkdownTheme } from "../../theme"
import {
escapeChars,
memberSymbol,
stripComments,
stripLineBreaks,
} from "../../utils"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"declarationTitle",
function (this: ParameterReflection | DeclarationReflection) {
const md = theme.hideMembersSymbol ? [] : [memberSymbol(this)]
function getType(
reflection: ParameterReflection | DeclarationReflection
) {
const reflectionType = reflection.type as ReflectionType
if (reflectionType && reflectionType.declaration?.children) {
return ": `Object`"
}
return (
(reflection.parent?.kindOf(ReflectionKind.Enum) ? " = " : ": ") +
Handlebars.helpers.type.call(
reflectionType ? reflectionType : reflection,
"object"
)
)
}
if (this.flags && this.flags.length > 0 && !this.flags.isRest) {
md.push(" " + this.flags.map((flag) => `\`${flag}\``).join(" "))
}
md.push(
`${this.flags.isRest ? "... " : ""} **${escapeChars(this.name)}**`
)
if (this instanceof DeclarationReflection && this.typeParameters) {
md.push(
`<${this.typeParameters
.map((typeParameter) => `\`${typeParameter.name}\``)
.join(", ")}\\>`
)
}
md.push(getType(this))
if (
!(this.type instanceof LiteralType) &&
this.defaultValue &&
this.defaultValue !== "..."
) {
md.push(` = \`${stripLineBreaks(stripComments(this.defaultValue))}\``)
}
return md.join("")
}
)
}
@@ -0,0 +1,8 @@
import * as Handlebars from "handlebars"
import { escapeChars } from "../../utils"
export default function () {
Handlebars.registerHelper("escape", function (str: string) {
return escapeChars(str)
})
}
@@ -0,0 +1,15 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"getFormattingOption",
function (optionName: string): unknown {
const options = theme.getFormattingOptionsForLocation()
return optionName in options
? options[optionName as keyof typeof options]
: null
}
)
}
@@ -0,0 +1,28 @@
import * as Handlebars from "handlebars"
import { DeclarationHierarchy } from "typedoc/dist/lib/models"
import { spaces } from "../../utils"
export default function () {
Handlebars.registerHelper(
"hierarchy",
function (this: DeclarationHierarchy, level: number) {
const md: string[] = []
const symbol = level > 0 ? getSymbol(level) : "-"
this.types.forEach((hierarchyType) => {
if (this.isTarget) {
md.push(`${symbol} **\`${hierarchyType}\`**`)
} else {
md.push(`${symbol} ${Handlebars.helpers.type.call(hierarchyType)}`)
}
})
if (this.next) {
md.push(Handlebars.helpers.hierarchy.call(this.next, level + 1))
}
return md.join("\n\n")
}
)
function getSymbol(level: number) {
return spaces(2) + [...Array(level)].map(() => "↳").join("")
}
}
@@ -0,0 +1,16 @@
import * as Handlebars from "handlebars"
import { DeclarationReflection, ReferenceReflection } from "typedoc"
export default function () {
Handlebars.registerHelper(
"ifIsReference",
function (
this: DeclarationReflection | ReferenceReflection,
options: Handlebars.HelperOptions
) {
return this instanceof ReferenceReflection
? options.fn(this)
: options.inverse(this)
}
)
}
@@ -0,0 +1,12 @@
import * as Handlebars from "handlebars"
import { PageEvent } from "typedoc"
import { MarkdownTheme } from "../../theme"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"ifNamedAnchors",
function (this: PageEvent, options: Handlebars.HelperOptions) {
return theme.namedAnchors ? options.fn(this) : options.inverse(this)
}
)
}
@@ -0,0 +1,12 @@
import * as Handlebars from "handlebars"
import { PageEvent } from "typedoc"
import { MarkdownTheme } from "../../theme"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"ifShowBreadcrumbs",
function (this: PageEvent, options: Handlebars.HelperOptions) {
return theme.hideBreadcrumbs ? options.inverse(this) : options.fn(this)
}
)
}
@@ -0,0 +1,12 @@
import * as Handlebars from "handlebars"
import { PageEvent } from "typedoc"
import { MarkdownTheme } from "../../theme"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"ifShowNamedAnchors",
function (this: PageEvent, options: Handlebars.HelperOptions) {
return theme.namedAnchors ? options.fn(this) : options.inverse(this)
}
)
}
@@ -0,0 +1,12 @@
import * as Handlebars from "handlebars"
import { PageEvent } from "typedoc"
import { MarkdownTheme } from "../../theme"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"ifShowPageTitle",
function (this: PageEvent, options: Handlebars.HelperOptions) {
return theme.hidePageTitle ? options.inverse(this) : options.fn(this)
}
)
}
@@ -0,0 +1,13 @@
import * as Handlebars from "handlebars"
import { ReflectionKind, SignatureReflection } from "typedoc"
export default function () {
Handlebars.registerHelper(
"ifShowReturns",
function (this: SignatureReflection, options: Handlebars.HelperOptions) {
return this.type && !this.parent?.kindOf(ReflectionKind.Constructor)
? options.fn(this)
: options.inverse(this)
}
)
}
@@ -0,0 +1,18 @@
import * as Handlebars from "handlebars"
import { DeclarationHierarchy, DeclarationReflection } from "typedoc"
import { PageEvent } from "typedoc"
export default function () {
Handlebars.registerHelper(
"ifShowTypeHierarchy",
function (
this: PageEvent<DeclarationReflection>,
options: Handlebars.HelperOptions
) {
const typeHierarchy = this.model?.typeHierarchy as DeclarationHierarchy
return typeHierarchy && typeHierarchy.next
? options.fn(this)
: options.inverse(this)
}
)
}
@@ -0,0 +1,22 @@
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
export default function () {
Handlebars.registerHelper(
"indexSignatureTitle",
function (this: SignatureReflection) {
const md = ["▪"]
const parameters = this.parameters
? this.parameters.map((parameter) => {
return `${parameter.name}: ${Handlebars.helpers.type.call(
parameter.type
)}`
})
: []
md.push(
`[${parameters.join("")}]: ${Handlebars.helpers.type.call(this.type)}`
)
return md.join(" ")
}
)
}
@@ -0,0 +1,28 @@
import * as Handlebars from "handlebars"
import reflectionFomatter from "../../utils/reflection-formatter"
import { ReflectionParameterType } from "../../types"
import { parseParams } from "../../utils/params-utils"
export default function () {
Handlebars.registerHelper(
"parameterList",
function (this: ReflectionParameterType[]) {
return list(
this.reduce(
(acc: ReflectionParameterType[], current) =>
parseParams(current, acc),
[]
)
)
}
)
}
function list(parameters: ReflectionParameterType[]) {
const items = parameters.map((parameter) => {
return reflectionFomatter(parameter)
})
return items.join("\n")
}
@@ -0,0 +1,119 @@
import * as Handlebars from "handlebars"
import { ParameterReflection, ReflectionType } from "typedoc"
import { stripLineBreaks } from "../../utils"
import { getReflectionType } from "./type"
import { parseParams } from "../../utils/params-utils"
import { ReflectionParameterType } from "../../types"
export default function () {
Handlebars.registerHelper(
"parameterTable",
function (this: ReflectionParameterType[]) {
return table(
this.reduce(
(acc: ReflectionParameterType[], current: ReflectionParameterType) =>
parseParams(current, acc),
[]
) as ParameterReflection[]
)
}
)
}
function table(parameters: ParameterReflection[]) {
const showDefaults = hasDefaultValues(parameters)
const comments = parameters.map(
(param: ParameterReflection) => !!param.comment?.hasVisibleComponent()
)
const hasComments = !comments.every((value) => !value)
const headers = ["Name", "Type"]
if (showDefaults) {
headers.push("Default value")
}
if (hasComments) {
headers.push("Description")
}
const rows = parameters.map((parameter) => {
const row: string[] = []
const nbsp = " " // ? <== Unicode no-break space character
const rest = parameter.flags.isRest ? "..." : ""
const optional = parameter.flags.isOptional ? "?" : ""
const isDestructuredParam = parameter.name == "__namedParameters"
const isDestructuredParamProp =
parameter.name.startsWith("__namedParameters.")
if (isDestructuredParam) {
row.push(`\`${rest}«destructured»\``)
} else if (isDestructuredParamProp) {
row.push(`${nbsp}\`${rest}${parameter.name.slice(18)}${optional}\``)
} else {
row.push(`\`${rest}${parameter.name}${optional}\``)
}
row.push(
parameter.type
? Handlebars.helpers.type.call(parameter.type, "object")
: getReflectionType(parameter, "object")
)
if (showDefaults) {
row.push(getDefaultValue(parameter))
}
if (hasComments) {
const comments = getComments(parameter)
if (comments) {
row.push(
stripLineBreaks(Handlebars.helpers.comments(comments)).replace(
/\|/g,
"\\|"
)
)
} else {
row.push("-")
}
}
return `| ${row.join(" | ")} |\n`
})
const output = `\n| ${headers.join(" | ")} |\n| ${headers
.map(() => ":------")
.join(" | ")} |\n${rows.join("")}`
return output
}
function getDefaultValue(parameter: ParameterReflection) {
return parameter.defaultValue && parameter.defaultValue !== "..."
? `\`${parameter.defaultValue}\``
: "`undefined`"
}
function hasDefaultValues(parameters: ParameterReflection[]) {
const defaultValues = (parameters as ParameterReflection[]).map(
(param) =>
param.defaultValue !== "{}" &&
param.defaultValue !== "..." &&
!!param.defaultValue
)
return !defaultValues.every((value) => !value)
}
function getComments(parameter: ParameterReflection) {
if (parameter.type instanceof ReflectionType) {
if (
parameter.type?.declaration?.signatures &&
parameter.type?.declaration?.signatures[0]?.comment
) {
return parameter.type?.declaration?.signatures[0]?.comment
}
}
return parameter.comment
}
@@ -0,0 +1,18 @@
import * as Handlebars from "handlebars"
import { MarkdownTheme } from "../../theme"
import { ParameterReflection } from "typedoc"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"parameter",
function (this: ParameterReflection[]) {
const { parameterStyle } = theme.getFormattingOptionsForLocation()
if (parameterStyle === "list") {
return Handlebars.helpers.parameterList.call(this)
} else {
return Handlebars.helpers.parameterTable.call(this)
}
}
)
}
@@ -0,0 +1,25 @@
import * as Handlebars from "handlebars"
import { ReferenceReflection } from "typedoc"
export default function () {
Handlebars.registerHelper(
"referenceMember",
function (this: ReferenceReflection) {
const referenced = this.tryGetTargetReflectionDeep()
if (!referenced) {
return `Re-exports ${this.name}`
}
if (this.name === referenced.name) {
return `Re-exports [${
referenced.name
}](${Handlebars.helpers.relativeURL(referenced.url)})`
}
return `Renames and re-exports [${
referenced.name
}](${Handlebars.helpers.relativeURL(referenced.url)})`
}
)
}
@@ -0,0 +1,36 @@
import * as Handlebars from "handlebars"
import { ContainerReflection, ReflectionKind } from "typedoc"
import { PageEvent } from "typedoc/dist/lib/output/events"
export default function () {
Handlebars.registerHelper(
"reflectionPath",
function (this: PageEvent<ContainerReflection>) {
if (this.model) {
if (this.model.kind && this.model.kind !== ReflectionKind.Module) {
const title: string[] = []
if (this.model.parent && this.model.parent.parent) {
if (this.model.parent.parent.parent) {
title.push(
`[${
this.model.parent.parent.name
}](${Handlebars.helpers.relativeURL(
this.model?.parent?.parent.url
)})`
)
}
title.push(
`[${this.model.parent.name}](${Handlebars.helpers.relativeURL(
this.model.parent.url
)})`
)
}
title.push(this.model.name)
return title.length > 1 ? `${title.join(".")}` : null
}
}
return null
}
)
}
@@ -0,0 +1,40 @@
import * as Handlebars from "handlebars"
import { PageEvent, ParameterReflection, ReflectionKind } from "typedoc"
import { MarkdownTheme } from "../../theme"
import { escapeChars, getDisplayName } from "../../utils"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"reflectionTitle",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function (this: PageEvent<any>, shouldEscape = true) {
const { reflectionTitle } = theme.getFormattingOptionsForLocation()
const title: string[] = [""]
if (
reflectionTitle?.kind &&
this.model?.kind &&
this.url !== this.project.url
) {
title.push(`${ReflectionKind.singularString(this.model.kind)}: `)
}
if (this.url === this.project.url) {
title.push(theme.indexTitle || getDisplayName(this.model))
} else {
title.push(
shouldEscape ? escapeChars(this.model.name) : this.model.name
)
if (reflectionTitle?.typeParameters && this.model.typeParameters) {
const typeParameters = this.model.typeParameters
.map((typeParameter: ParameterReflection) => typeParameter.name)
.join(", ")
title.push(`<${typeParameters}${shouldEscape ? "\\>" : ">"}`)
}
}
if (reflectionTitle?.suffix) {
title.push(` ${reflectionTitle.suffix}`)
}
return title.join("")
}
)
}
@@ -0,0 +1,13 @@
import * as Handlebars from "handlebars"
import { MarkdownTheme } from "../../theme"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper("relativeURL", function (url: string) {
return url
? theme.publicPath
? theme.publicPath + url
: theme.getRelativeUrl(url)
: url
})
}
@@ -0,0 +1,34 @@
import * as Handlebars from "handlebars"
import { Comment, DeclarationReflection } from "typedoc"
import reflectionFomatter from "../../utils/reflection-formatter"
export default function () {
Handlebars.registerHelper("returns", function (comment: Comment) {
const md: string[] = []
if (comment.blockTags?.length) {
const tags = comment.blockTags
.filter((tag) => tag.tag === "@returns")
.map((tag) => {
let result = Handlebars.helpers.comment(tag.content)
tag.content.forEach((commentPart) => {
if (
"target" in commentPart &&
commentPart.target instanceof DeclarationReflection
) {
const content = commentPart.target.children?.map((childItem) =>
reflectionFomatter(childItem)
)
result += `\n\n<details>\n<summary>\n${
commentPart.target.name
}\n</summary>\n\n${content?.join("\n")}\n\n</details>`
}
})
return result
})
md.push(tags.join("\n\n"))
}
return md.join("")
})
}
@@ -0,0 +1,13 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"sectionEnabled",
function (sectionName: string): boolean {
const { sections } = theme.getFormattingOptionsForLocation()
return !sections || !(sectionName in sections) || sections[sectionName]
}
)
}
@@ -0,0 +1,60 @@
import * as Handlebars from "handlebars"
import {
ParameterReflection,
ReflectionKind,
SignatureReflection,
} from "typedoc"
import { memberSymbol } from "../../utils"
import { MarkdownTheme } from "../../theme"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"signatureTitle",
function (this: SignatureReflection, accessor?: string, standalone = true) {
const md: string[] = []
if (standalone && !theme.hideMembersSymbol) {
md.push(`${memberSymbol(this)} `)
}
if (this.parent && this.parent.flags?.length > 0) {
md.push(this.parent.flags.map((flag) => `\`${flag}\``).join(" ") + " ")
}
if (accessor) {
md.push(`\`${accessor}\` **${this.name}**`)
} else if (this.name !== "__call" && this.name !== "__type") {
md.push(`**${this.name}**`)
}
if (this.typeParameters) {
md.push(
`<${this.typeParameters
.map((typeParameter) => `\`${typeParameter.name}\``)
.join(", ")}\\>`
)
}
md.push(`(${getParameters(this.parameters)})`)
if (this.type && !this.parent?.kindOf(ReflectionKind.Constructor)) {
md.push(`: ${Handlebars.helpers.type.call(this.type, "object")}`)
}
return md.join("") + (standalone ? "\n" : "")
}
)
}
const getParameters = (
parameters: ParameterReflection[] = [],
backticks = true
) => {
return parameters
.map((param) => {
const isDestructuredParam = param.name == "__namedParameters"
const paramItem = `${param.flags.isRest ? "..." : ""}${
isDestructuredParam ? "«destructured»" : param.name
}${param.flags.isOptional || param.defaultValue ? "?" : ""}`
return backticks ? `\`${paramItem}\`` : paramItem
})
.join(", ")
}
@@ -0,0 +1,12 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper("titleLevel", function (originalLevel = 3): string {
const { expandMembers } = theme.getFormattingOptionsForLocation()
const level = expandMembers ? originalLevel - 1 : originalLevel
return Array(level).fill("#").join("")
})
}
@@ -0,0 +1,57 @@
import * as Handlebars from "handlebars"
import {
DeclarationReflection,
ProjectReflection,
ReflectionGroup,
} from "typedoc"
import { MarkdownTheme } from "../../theme"
import { escapeChars } from "../../utils"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"toc",
function (this: ProjectReflection | DeclarationReflection) {
const md: string[] = []
const { hideInPageTOC } = theme
const isVisible = this.groups?.some((group) =>
group.allChildrenHaveOwnDocument()
)
function pushGroup(group: ReflectionGroup, md: string[]) {
const children = group.children.map(
(child) =>
`- [${escapeChars(child.name)}](${Handlebars.helpers.relativeURL(
child.url
)})`
)
md.push(children.join("\n"))
}
if ((!hideInPageTOC && this.groups) || (isVisible && this.groups)) {
if (!hideInPageTOC) {
md.push(`## Table of contents\n\n`)
}
const headingLevel = hideInPageTOC ? `##` : `###`
this.groups?.forEach((group) => {
const groupTitle = group.title
if (group.categories) {
group.categories.forEach((category) => {
md.push(`${headingLevel} ${category.title} ${groupTitle}\n\n`)
pushGroup(category as ReflectionGroup, md)
md.push("\n")
})
} else {
if (!hideInPageTOC || group.allChildrenHaveOwnDocument()) {
md.push(`${headingLevel} ${groupTitle}\n\n`)
pushGroup(group, md)
md.push("\n")
}
}
})
}
return md.length > 0 ? md.join("\n") : null
}
)
}
@@ -0,0 +1,57 @@
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
import { ArrayType, ReferenceType } from "typedoc/dist/lib/models/types"
import { escapeChars } from "../../utils"
export default function () {
Handlebars.registerHelper(
"typeAndParent",
function (this: ArrayType | ReferenceType) {
const getUrl = (name: string, url: string) =>
`[${name}](${Handlebars.helpers.relativeURL(url)})`
if (this) {
if ("elementType" in this) {
return Handlebars.helpers.typeAndParent.call(this.elementType) + "[]"
} else {
if (this.reflection) {
const md: string[] = []
if (this.reflection instanceof SignatureReflection) {
if (this.reflection.parent?.parent?.url) {
md.push(
getUrl(
this.reflection.parent.parent.name,
this.reflection.parent.parent.url
)
)
if (this.reflection.parent.url) {
md.push(
getUrl(
this.reflection.parent.name,
this.reflection.parent.url
)
)
}
}
} else {
if (this.reflection.parent?.url) {
md.push(
getUrl(
this.reflection.parent.name,
this.reflection.parent.url
)
)
if (this.reflection.url) {
md.push(getUrl(this.reflection.name, this.reflection.url))
}
}
}
return md.join(".")
} else {
return escapeChars(this.toString())
}
}
}
return "void"
}
)
}
@@ -0,0 +1,139 @@
import * as Handlebars from "handlebars"
import { DeclarationReflection, ReflectionType } from "typedoc"
import { MarkdownTheme } from "../../theme"
import { escapeChars, stripLineBreaks } from "../../utils"
import reflectionFomatter from "../../utils/reflection-formatter"
import { parseParams } from "../../utils/params-utils"
import { ReflectionParameterType } from "../../types"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"typeDeclarationMembers",
function (this: DeclarationReflection[]) {
const comments = this.map(
(param) => !!param.comment?.hasVisibleComponent()
)
const hasComments = !comments.every((value) => !value)
const properties = this.reduce(
(acc: ReflectionParameterType[], current: ReflectionParameterType) =>
parseParams(current, acc),
[]
) as DeclarationReflection[]
let result = ""
switch (theme.objectLiteralTypeDeclarationStyle) {
case "list": {
result = getListMarkdownContent(properties)
break
}
case "table": {
result = getTableMarkdownContent(properties, hasComments)
break
}
}
return result
}
)
}
function getListMarkdownContent(properties: DeclarationReflection[]) {
const items = properties.map((property) => reflectionFomatter(property))
return items.join("\n")
}
function getTableMarkdownContent(
properties: DeclarationReflection[],
hasComments: boolean
) {
const headers = ["Name", "Type"]
if (hasComments) {
headers.push("Description")
}
const rows = properties.map((property) => {
const propertyType = getPropertyType(property)
const row: string[] = []
const nameCol: string[] = []
const name =
property.name.match(/[\\`\\|]/g) !== null
? escapeChars(getName(property))
: `\`${getName(property)}\``
nameCol.push(name)
row.push(nameCol.join(" "))
row.push(
Handlebars.helpers.type.call(propertyType).replace(/(?<!\\)\|/g, "\\|")
)
if (hasComments) {
const comments = getComments(property)
if (comments) {
row.push(
stripLineBreaks(Handlebars.helpers.comments(comments)).replace(
/\|/g,
"\\|"
)
)
} else {
row.push("-")
}
}
return `| ${row.join(" | ")} |\n`
})
const output = `\n| ${headers.join(" | ")} |\n| ${headers
.map(() => ":------")
.join(" | ")} |\n${rows.join("")}`
return output
}
function getPropertyType(property: DeclarationReflection) {
if (property.getSignature) {
return property.getSignature.type
}
if (property.setSignature) {
return property.setSignature.type
}
return property.type ? property.type : property
}
function getName(property: DeclarationReflection) {
const md: string[] = []
if (property.flags.isRest) {
md.push("...")
}
if (property.getSignature) {
md.push(`get ${property.getSignature.name}()`)
} else if (property.setSignature) {
md.push(
`set ${
property.setSignature.name
}(${property.setSignature.parameters?.map((parameter) => {
return `${parameter.name}:${Handlebars.helpers.type.call(
parameter.type,
"all",
false
)}`
})})`
)
} else {
md.push(property.name)
}
if (property.flags.isOptional) {
md.push("?")
}
return md.join("")
}
function getComments(property: DeclarationReflection) {
if (property.type instanceof ReflectionType) {
if (property.type?.declaration?.signatures) {
return property.type?.declaration.signatures[0].comment
}
}
if (property.signatures?.length) {
return property.signatures[0].comment
}
return property.comment
}
@@ -0,0 +1,18 @@
import * as Handlebars from "handlebars"
import { TypeParameterReflection } from "typedoc"
import reflectionFomatter from "../../utils/reflection-formatter"
export default function () {
Handlebars.registerHelper(
"typeParameterList",
function (this: TypeParameterReflection[]) {
return list(this)
}
)
}
function list(parameters: TypeParameterReflection[]) {
const items = parameters.map((parameter) => reflectionFomatter(parameter))
return items.join("\n")
}
@@ -0,0 +1,82 @@
import * as Handlebars from "handlebars"
import { TypeParameterReflection } from "typedoc"
import { stripLineBreaks } from "../../utils"
export default function () {
Handlebars.registerHelper(
"typeParameterTable",
function (this: TypeParameterReflection[]) {
return table(this)
}
)
}
function table(parameters: TypeParameterReflection[]) {
const showTypeCol = hasTypes(parameters)
const comments = parameters.map(
(param: TypeParameterReflection) => !!param.comment?.hasVisibleComponent()
)
const hasComments = !comments.every((value) => !value)
const headers = ["Name"]
if (showTypeCol) {
headers.push("Type")
}
if (hasComments) {
headers.push("Description")
}
const rows = parameters.map((parameter: TypeParameterReflection) => {
const row: string[] = []
row.push(`\`${parameter.name}\``)
if (showTypeCol) {
const typeCol: string[] = []
if (!parameter.type && !parameter.default) {
typeCol.push(`\`${parameter.name}\``)
}
if (parameter.type) {
typeCol.push(
`extends ${Handlebars.helpers.type.call(parameter.type, "object")}`
)
}
if (parameter.default) {
if (parameter.type) {
typeCol.push(" = ")
}
typeCol.push(Handlebars.helpers.type.call(parameter.default))
}
row.push(typeCol.join(""))
}
if (hasComments) {
if (parameter.comment?.summary) {
row.push(
stripLineBreaks(
Handlebars.helpers.comment(parameter.comment?.summary)
).replace(/\|/g, "\\|")
)
} else {
row.push("-")
}
}
return `| ${row.join(" | ")} |\n`
})
const output = `\n| ${headers.join(" | ")} |\n| ${headers
.map(() => ":------")
.join(" | ")} |\n${rows.join("")}`
return output
}
function hasTypes(parameters: TypeParameterReflection[]) {
const types = (parameters as TypeParameterReflection[]).map(
(param) => !!param.type || !!param.default
)
return !types.every((value) => !value)
}
@@ -0,0 +1,18 @@
import * as Handlebars from "handlebars"
import { MarkdownTheme } from "../../theme"
import { TypeParameterReflection } from "typedoc"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"typeParameter",
function (this: TypeParameterReflection[]) {
const { parameterStyle } = theme.getFormattingOptionsForLocation()
if (parameterStyle === "list") {
return Handlebars.helpers.typeParameterList.call(this)
} else {
return Handlebars.helpers.typeParameterTable.call(this)
}
}
)
}
@@ -0,0 +1,306 @@
import * as Handlebars from "handlebars"
import {
ArrayType,
ConditionalType,
DeclarationReflection,
IndexedAccessType,
InferredType,
IntersectionType,
IntrinsicType,
LiteralType,
PredicateType,
QueryType,
ReferenceType,
ReflectionType,
SignatureReflection,
TupleType,
TypeOperatorType,
UnionType,
UnknownType,
} from "typedoc"
import { escapeChars } from "../../utils"
import { ReflectionParameterType } from "../../types"
type Collapse = "object" | "function" | "all" | "none"
export default function () {
Handlebars.registerHelper(
"type",
function (
this:
| ArrayType
| IntersectionType
| IntrinsicType
| ReferenceType
| TupleType
| UnionType
| TypeOperatorType
| QueryType
| PredicateType
| ReferenceType
| ConditionalType
| IndexedAccessType
| UnknownType
| InferredType,
collapse: Collapse = "none",
emphasis = true
) {
if (this instanceof ReferenceType) {
return getReferenceType(this, emphasis)
}
if (this instanceof ArrayType && this.elementType) {
return getArrayType(this, emphasis)
}
if (this instanceof UnionType && this.types) {
return getUnionType(this, emphasis)
}
if (this instanceof IntersectionType && this.types) {
return getIntersectionType(this)
}
if (this instanceof TupleType && this.elements) {
return getTupleType(this)
}
if (this instanceof IntrinsicType && this.name) {
return getIntrinsicType(this, emphasis)
}
if (this instanceof ReflectionType) {
return getReflectionType(this, collapse)
}
if (this instanceof DeclarationReflection) {
return getReflectionType(this, collapse)
}
if (this instanceof TypeOperatorType) {
return getTypeOperatorType(this)
}
if (this instanceof QueryType) {
return getQueryType(this)
}
if (this instanceof ConditionalType) {
return getConditionalType(this)
}
if (this instanceof IndexedAccessType) {
return getIndexAccessType(this)
}
if (this instanceof UnknownType) {
return getUnknownType(this)
}
if (this instanceof InferredType) {
return getInferredType(this)
}
if (this instanceof LiteralType) {
return getLiteralType(this)
}
return this ? escapeChars(this.toString()) : ""
}
)
}
function getLiteralType(model: LiteralType) {
if (typeof model.value === "bigint") {
return `\`${model.value}n\``
}
return `\`\`${JSON.stringify(model.value)}\`\``
}
export function getReflectionType(
model: ReflectionParameterType,
collapse: Collapse
) {
const root = model instanceof ReflectionType ? model.declaration : model
if ("signatures" in root && root.signatures) {
return collapse === "function" || collapse === "all"
? `\`fn\``
: getFunctionType(root.signatures)
}
return collapse === "object" || collapse === "all"
? `\`Object\``
: getDeclarationType(root as DeclarationReflection)
}
export function getDeclarationType(model: DeclarationReflection) {
if (model.indexSignature || model.children) {
let indexSignature = ""
const declarationIndexSignature = model.indexSignature
if (declarationIndexSignature) {
const key = declarationIndexSignature.parameters
? declarationIndexSignature.parameters.map(
(param) => `\`[${param.name}: ${param.type}]\``
)
: ""
const obj = Handlebars.helpers.type.call(declarationIndexSignature.type)
indexSignature = `${key}: ${obj}; `
}
const types =
model.children &&
model.children.map((obj) => {
return `\`${obj.name}${
obj.flags.isOptional ? "?" : ""
}\`: ${Handlebars.helpers.type.call(
obj.signatures || obj.children ? obj : obj.type
)} ${
obj.defaultValue && obj.defaultValue !== "..."
? `= ${escapeChars(obj.defaultValue)}`
: ""
}`
})
return `{ ${indexSignature ? indexSignature : ""}${
types ? types.join("; ") : ""
} }${
model.defaultValue && model.defaultValue !== "..."
? `= ${escapeChars(model.defaultValue)}`
: ""
}`
}
return "{}"
}
export function getFunctionType(modelSignatures: SignatureReflection[]) {
const functions = modelSignatures.map((fn) => {
const typeParams = fn.typeParameters
? `<${fn.typeParameters
.map((typeParameter) => typeParameter.name)
.join(", ")}\\>`
: []
const params = fn.parameters
? fn.parameters.map((param) => {
return `${param.flags.isRest ? "..." : ""}\`${param.name}${
param.flags.isOptional ? "?" : ""
}\`: ${Handlebars.helpers.type.call(param.type ? param.type : param)}`
})
: []
const returns = Handlebars.helpers.type.call(fn.type)
return typeParams + `(${params.join(", ")}) => ${returns}`
})
return functions.join("")
}
export function getReferenceType(model: ReferenceType, emphasis: boolean) {
if (model.reflection || (model.name && model.typeArguments)) {
const reflection: string[] = []
if (model.reflection?.url) {
reflection.push(
`[${`\`${model.reflection.name}\``}](${Handlebars.helpers.relativeURL(
model.reflection.url
)})`
)
} else {
reflection.push(
model.externalUrl
? `[${`\`${model.name}\``}]( ${model.externalUrl} )`
: `\`${model.name}\``
)
}
if (model.typeArguments && model.typeArguments.length > 0) {
reflection.push(
`<${model.typeArguments
.map((typeArgument) => Handlebars.helpers.type.call(typeArgument))
.join(", ")}\\>`
)
}
return reflection.join("")
}
return emphasis
? model.externalUrl
? `[${`\`${model.name}\``}]( ${model.externalUrl} )`
: `\`${model.name}\``
: escapeChars(model.name)
}
export function getArrayType(model: ArrayType, emphasis: boolean) {
const arrayType = Handlebars.helpers.type.call(
model.elementType,
"none",
emphasis
)
return model.elementType.type === "union"
? `(${arrayType})[]`
: `${arrayType}[]`
}
export function getUnionType(model: UnionType, emphasis: boolean) {
return model.types
.map((unionType) =>
Handlebars.helpers.type.call(unionType, "none", emphasis)
)
.join(` \\| `)
}
export function getIntersectionType(model: IntersectionType) {
return model.types
.map((intersectionType) => Handlebars.helpers.type.call(intersectionType))
.join(" & ")
}
export function getTupleType(model: TupleType) {
return `[${model.elements
.map((element) => Handlebars.helpers.type.call(element))
.join(", ")}]`
}
export function getIntrinsicType(model: IntrinsicType, emphasis: boolean) {
return emphasis ? `\`${model.name}\`` : escapeChars(model.name)
}
export function getTypeOperatorType(model: TypeOperatorType) {
return `${model.operator} ${Handlebars.helpers.type.call(model.target)}`
}
export function getQueryType(model: QueryType) {
return `typeof ${Handlebars.helpers.type.call(model.queryType)}`
}
export function getInferredType(model: InferredType) {
return `infer ${escapeChars(model.name)}`
}
export function getUnknownType(model: UnknownType) {
return escapeChars(model.name)
}
export function getConditionalType(model: ConditionalType) {
const md: string[] = []
if (model.checkType) {
md.push(Handlebars.helpers.type.call(model.checkType))
}
md.push("extends")
if (model.extendsType) {
md.push(Handlebars.helpers.type.call(model.extendsType))
}
md.push("?")
if (model.trueType) {
md.push(Handlebars.helpers.type.call(model.trueType))
}
md.push(":")
if (model.falseType) {
md.push(Handlebars.helpers.type.call(model.falseType))
}
return md.join(" ")
}
export function getIndexAccessType(model: IndexedAccessType) {
const md: string[] = []
if (model.objectType) {
md.push(Handlebars.helpers.type.call(model.objectType))
}
if (model.indexType) {
md.push(`[${Handlebars.helpers.type.call(model.indexType)}]`)
}
return md.join("")
}
@@ -0,0 +1,13 @@
{{#if (sectionEnabled "comment")}}
{{#with comment}}
{{#if hasVisibleComponent}}
{{{comments this}}}
{{/if}}
{{/with}}
{{/if}}
@@ -0,0 +1,5 @@
{{#ifShowBreadcrumbs}}
{{{breadcrumbs}}}
{{/ifShowBreadcrumbs}}
@@ -0,0 +1,3 @@
{{ toc }}
{{> members}}
@@ -0,0 +1,87 @@
{{{declarationTitle}}}
{{> comment}}
{{#if (sectionEnabled "member_declaration_typeParameters")}}
{{#if typeParameters}}
{{titleLevel 4}} Type parameters
{{#with typeParameters}}
{{{typeParameter}}}
{{/with}}
{{/if}}
{{/if}}
{{#if type.declaration}}
{{#if (sectionEnabled "member_declaration_indexSignature")}}
{{#if type.declaration.indexSignature}}
{{#with type.declaration.indexSignature}}
{{titleLevel 4}} Index signature
{{{indexSignatureTitle}}}
{{> comment}}
{{/with}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "member_declaration_signatures")}}
{{#if type.declaration.signatures}}
{{#if type.declaration.children}}
{{titleLevel 4}} Call signature
{{else}}
{{titleLevel 4}} Type declaration
{{/if}}
{{#each type.declaration.signatures}}
{{> member.signature showSources=false }}
{{/each}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "member_declaration_typeDeclaration")}}
{{#if type.declaration.children}}
{{#with type.declaration}}
{{titleLevel 4}} Type declaration
{{/with}}
{{#with type.declaration.children}}
{{{typeDeclarationMembers}}}
{{/with}}
{{/if}}
{{/if}}
{{/if}}
{{> member.sources}}
@@ -0,0 +1,27 @@
{{#if getSignature}}
{{#if (sectionEnabled "member_getteSetter_getSignature")}}
{{#with getSignature}}
{{> member.signature accessor="get" showSources=true }}
{{/with}}
{{/if}}
{{/if}}
{{#if setSignature}}
{{#if (sectionEnabled "member_getteSetter_setSignature")}}
{{#with setSignature}}
{{> member.signature accessor="set" showSources=true }}
{{/with}}
{{/if}}
{{/if}}
@@ -0,0 +1,61 @@
{{#unless hasOwnDocument}}
{{#if name}}
{{titleLevel 3}} {{#ifNamedAnchors}}<a id="{{anchor}}" name="{{this.anchor}}"></a> {{/ifNamedAnchors}}{{ escape name }}
{{/if}}
{{/unless}}
{{#if signatures}}
{{#if (sectionEnabled "member_signatures")}}
{{#each signatures}}
{{> member.signature showSources=true }}
{{/each}}
{{/if}}
{{else}}
{{#if hasGetterOrSetter}}
{{#if (sectionEnabled "member_getterSetter")}}
{{> member.getterSetter}}
{{/if}}
{{else}}
{{#ifIsReference}}
{{#if (sectionEnabled "member_reference")}}
{{referenceMember}}
{{/if}}
{{else}}
{{#if (sectionEnabled "member_declaration")}}
{{> member.declaration}}
{{/if}}
{{/ifIsReference}}
{{/if}}
{{/if}}
{{#unless @last}}
{{#unless hasOwnDocument}}
___
{{/unless}}
{{/unless}}
@@ -0,0 +1,147 @@
{{#if (sectionEnabled "member_signature_title")}}
{{{signatureTitle accessor}}}
{{/if}}
{{#if (sectionEnabled "member_signature_comment")}}
{{#with comment}}
{{#if hasVisibleComponent}}
{{{comments this true false}}}
{{/if}}
{{/with}}
{{/if}}
{{#if (sectionEnabled "member_signature_typeParameters")}}
{{#if typeParameters}}
{{#with typeParameters}}
{{{typeParameter}}}
{{/with}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "member_signature_parameters")}}
{{#if parameters}}
{{#if showSources}}
{{titleLevel 4}} Parameters
{{else}}
{{titleLevel 5}} Parameters
{{/if}}
{{#with parameters}}
{{{parameter}}}
{{/with}}
{{/if}}
{{/if}}
{{#ifShowReturns}}
{{#if type}}
{{#if showSources}}
{{titleLevel 4}} Returns
{{else}}
{{titleLevel 5}} Returns
{{/if}}
{{#if (getFormattingOption "showReturnSignature")}}
{{#with type}}
{{{type 'all'}}}
{{/with}}
{{/if}}
{{#with comment}}
{{{returns this}}}
{{/with}}
{{#with type}}
{{#if (sectionEnabled "member_signature_declarationSignatures")}}
{{#if declaration.signatures}}
{{#each declaration.signatures}}
{{> member.signature showSources=false }}
{{/each}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "member_signature_declarationChildren")}}
{{#if declaration.children}}
{{#with declaration.children}}
{{{typeDeclarationMembers}}}
{{/with}}
{{/if}}
{{/if}}
{{/with}}
{{/if}}
{{/ifShowReturns}}
{{#if (sectionEnabled "member_signature_comment")}}
{{#with comment}}
{{#if hasVisibleComponent}}
{{{comments this false true 4}}}
{{/if}}
{{/with}}
{{/if}}
{{#if (sectionEnabled "member_signature_sources")}}
{{#if showSources}}
{{> member.sources}}
{{/if}}
{{/if}}
@@ -0,0 +1,71 @@
{{#if (sectionEnabled "member_sources_implementationOf")}}
{{#if implementationOf}}
{{titleLevel 4}} Implementation of
{{#with implementationOf}}
{{typeAndParent}}
{{/with}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "member_sources_inheritedFrom")}}
{{#if inheritedFrom}}
{{titleLevel 4}} Inherited from
{{#with inheritedFrom}}
{{{typeAndParent}}}
{{/with}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "member_sources_overrides")}}
{{#if overwrites}}
{{titleLevel 4}} Overrides
{{#with overwrites}}
{{typeAndParent}}
{{/with}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "member_sources_definedIn")}}
{{#if sources}}
{{titleLevel 4}} Defined in
{{#each sources}}
{{#if url}}
[{{fileName}}:{{line}}]({{url}})
{{else}}
{{fileName}}:{{line}}
{{/if}}
{{/each}}
{{/if}}
{{/if}}
@@ -0,0 +1,41 @@
{{#if (sectionEnabled "members_group_categories")}}
{{#if categories}}
{{#each categories}}
{{#unless @first}}
___
{{/unless}}
{{#unless (getFormattingOption "expandMembers")}}
## {{title}} {{../title}}
{{/unless}}
{{#each children}}
{{> member}}
{{/each}}
{{/each}}
{{else}}
{{#unless (getFormattingOption "expandMembers")}}
## {{title}}
{{/unless}}
{{#each children}}
{{> member}}
{{/each}}
{{/if}}
{{/if}}
@@ -0,0 +1,43 @@
{{#if (sectionEnabled "members_categories")}}
{{#if categories}}
{{#each categories}}
{{#unless allChildrenHaveOwnDocument}}
{{#unless (getFormattingOption "expandMembers")}}
## {{title}}
{{/unless}}
{{#each children}}
{{#unless hasOwnDocument}}
{{> member}}
{{/unless}}
{{/each}}
{{/unless}}
{{/each}}
{{else}}
{{#each groups}}
{{#unless allChildrenHaveOwnDocument}}
{{> members.group}}
{{/unless}}
{{/each}}
{{/if}}
{{/if}}
@@ -0,0 +1,13 @@
{{#ifShowPageTitle}}
# {{{reflectionTitle true}}}
{{/ifShowPageTitle}}
{{{getFormattingOption "reflectionDescription"}}}
{{#if (sectionEnabled "title_reflectionPath")}}
{{{reflectionPath}}}
{{/if}}
@@ -0,0 +1,7 @@
{{> header}}
{{#with model.readme}}
{{{comment this}}}
{{/with}}
@@ -0,0 +1,127 @@
{{> header}}
{{> title}}
{{#with model}}
{{#if (sectionEnabled "reflection_comment")}}
{{#if hasComment}}
{{> comment}}
{{/if}}
{{/if}}
{{/with}}
{{#if (sectionEnabled "reflection_typeParameters")}}
{{#if model.typeParameters}}
## Type parameters
{{#with model.typeParameters}}
{{{typeParameter}}}
{{/with}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "reflection_hierarchy")}}
{{#ifShowTypeHierarchy}}
## Hierarchy
{{#with model.typeHierarchy}}
{{{hierarchy 0}}}
{{/with}}
{{/ifShowTypeHierarchy}}
{{/if}}
{{#if (sectionEnabled "reflection_implements")}}
{{#if model.implementedTypes}}
## Implements
{{#each model.implementedTypes}}
- {{{type}}}
{{/each}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "reflection_implementedBy")}}
{{#if model.implementedBy}}
## Implemented by
{{#each model.implementedBy}}
- {{{type}}}
{{/each}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "reflection_callable")}}
{{#if model.signatures}}
## Callable
{{#with model}}
{{#each signatures}}
### {{name}}
{{> member.signature showSources=true }}
{{/each}}
{{/with}}
{{/if}}
{{/if}}
{{#if (sectionEnabled "reflection_indexable")}}
{{#if model.indexSignature}}
## Indexable
{{#with model}}
{{#with indexSignature}}
{{{indexSignatureTitle}}}
{{> comment}}
{{/with}}
{{/with}}
{{/if}}
{{/if}}
{{#with model}}
{{> main}}
{{/with}}
@@ -0,0 +1,9 @@
{{> header}}
{{> title}}
{{#with model}}
{{> member}}
{{/with}}
@@ -0,0 +1,454 @@
import * as path from "path"
import {
ContainerReflection,
DeclarationReflection,
PageEvent,
ProjectReflection,
Reflection,
ReflectionGroup,
ReflectionKind,
RenderTemplate,
Renderer,
RendererEvent,
Theme,
UrlMapping,
} from "typedoc"
import { getKindPlural } from "./groups"
import {
indexTemplate,
reflectionMemberTemplate,
reflectionTemplate,
registerHelpers,
registerPartials,
} from "./render-utils"
import { formatContents } from "./utils"
import {
FormattingOptionType,
FormattingOptionsType,
Mapping,
NavigationItem,
ObjectLiteralDeclarationStyle,
} from "./types"
export class MarkdownTheme extends Theme {
allReflectionsHaveOwnDocument!: boolean
entryDocument: string
entryPoints!: string[]
filenameSeparator!: string
hideBreadcrumbs!: boolean
hideInPageTOC!: boolean
hidePageTitle!: boolean
hideMembersSymbol!: boolean
includes!: string
indexTitle!: string
mediaDirectory!: string
namedAnchors!: boolean
readme!: string
out!: string
publicPath!: string
preserveAnchorCasing!: boolean
objectLiteralTypeDeclarationStyle: ObjectLiteralDeclarationStyle
formattingOptions: FormattingOptionsType
project?: ProjectReflection
reflection?: DeclarationReflection
location!: string
anchorMap: Record<string, string[]> = {}
static URL_PREFIX = /^(http|ftp)s?:\/\//
constructor(renderer: Renderer) {
super(renderer)
// prettier-ignore
this.allReflectionsHaveOwnDocument = this.getOption("allReflectionsHaveOwnDocument") as boolean
this.entryDocument = this.getOption("entryDocument") as string
this.entryPoints = this.getOption("entryPoints") as string[]
this.filenameSeparator = this.getOption("filenameSeparator") as string
this.hideBreadcrumbs = this.getOption("hideBreadcrumbs") as boolean
this.hideInPageTOC = this.getOption("hideInPageTOC") as boolean
this.hidePageTitle = this.getOption("hidePageTitle") as boolean
this.hideMembersSymbol = this.getOption("hideMembersSymbol") as boolean
this.includes = this.getOption("includes") as string
this.indexTitle = this.getOption("indexTitle") as string
this.mediaDirectory = this.getOption("media") as string
this.namedAnchors = this.getOption("namedAnchors") as boolean
this.readme = this.getOption("readme") as string
this.out = this.getOption("out") as string
this.publicPath = this.getOption("publicPath") as string
this.preserveAnchorCasing = this.getOption(
"preserveAnchorCasing"
) as boolean
this.objectLiteralTypeDeclarationStyle = this.getOption(
"objectLiteralTypeDeclarationStyle"
) as ObjectLiteralDeclarationStyle
this.formattingOptions = this.getOption(
"formatting"
) as FormattingOptionsType
this.listenTo(this.owner, {
[RendererEvent.BEGIN]: this.onBeginRenderer,
[PageEvent.BEGIN]: this.onBeginPage,
})
registerPartials()
registerHelpers(this)
}
render(
page: PageEvent<Reflection>,
template: RenderTemplate<PageEvent<Reflection>>
): string {
return formatContents(template(page) as string)
}
getOption(key: string) {
return this.application.options.getValue(key)
}
getUrls(project: ProjectReflection) {
const urls: UrlMapping[] = []
const noReadmeFile = this.readme.endsWith("none")
if (noReadmeFile) {
project.url = this.entryDocument
urls.push(
new UrlMapping(
this.entryDocument,
project,
this.getReflectionTemplate()
)
)
} else {
project.url = this.globalsFile
urls.push(
new UrlMapping(this.globalsFile, project, this.getReflectionTemplate())
)
urls.push(
new UrlMapping(this.entryDocument, project, this.getIndexTemplate())
)
}
project.children?.forEach((child: Reflection) => {
if (child instanceof DeclarationReflection) {
this.buildUrls(child as DeclarationReflection, urls)
}
})
return urls
}
buildUrls(
reflection: DeclarationReflection,
urls: UrlMapping[]
): UrlMapping[] {
const mapping = this.mappings.find((mapping) =>
reflection.kindOf(mapping.kind)
)
if (mapping) {
if (!reflection.url || !MarkdownTheme.URL_PREFIX.test(reflection.url)) {
const url = this.toUrl(mapping, reflection)
urls.push(new UrlMapping(url, reflection, mapping.template))
reflection.url = url
reflection.hasOwnDocument = true
}
for (const child of reflection.children || []) {
if (mapping.isLeaf) {
this.applyAnchorUrl(child, reflection)
} else {
this.buildUrls(child, urls)
}
}
} else if (reflection.parent) {
this.applyAnchorUrl(reflection, reflection.parent, true)
}
return urls
}
toUrl(mapping: Mapping, reflection: DeclarationReflection) {
return mapping.directory + "/" + this.getUrl(reflection) + ".md"
}
getUrl(reflection: Reflection, relative?: Reflection): string {
let url = reflection.getAlias()
if (
reflection.parent &&
reflection.parent !== relative &&
!(reflection.parent instanceof ProjectReflection)
) {
url =
this.getUrl(reflection.parent, relative) + this.filenameSeparator + url
}
return url.replace(/^_/, "")
}
applyAnchorUrl(
reflection: Reflection,
container: Reflection,
isSymbol = false
) {
if (
container.url &&
(!reflection.url || !MarkdownTheme.URL_PREFIX.test(reflection.url))
) {
const reflectionId = this.preserveAnchorCasing
? reflection.name
: reflection.name.toLowerCase()
if (isSymbol) {
this.anchorMap[container.url]
? this.anchorMap[container.url].push(reflectionId)
: (this.anchorMap[container.url] = [reflectionId])
}
const count = this.anchorMap[container.url]?.filter(
(id) => id === reflectionId
)?.length
const anchor = this.toAnchorRef(
reflectionId + (count > 1 ? "-" + (count - 1).toString() : "")
)
reflection.url = container.url + "#" + anchor
reflection.anchor = anchor
reflection.hasOwnDocument = false
}
reflection.traverse((child) => {
if (child instanceof DeclarationReflection) {
this.applyAnchorUrl(child, container)
}
})
}
toAnchorRef(reflectionId: string) {
return reflectionId
}
getRelativeUrl(absolute: string) {
if (MarkdownTheme.URL_PREFIX.test(absolute)) {
return absolute
} else {
const relative = path.relative(
path.dirname(this.location),
path.dirname(absolute)
)
return path.join(relative, path.basename(absolute)).replace(/\\/g, "/")
}
}
getReflectionTemplate() {
return (pageEvent: PageEvent<ContainerReflection>) => {
return reflectionTemplate(pageEvent, {
allowProtoMethodsByDefault: true,
allowProtoPropertiesByDefault: true,
data: { theme: this },
})
}
}
getReflectionMemberTemplate() {
return (pageEvent: PageEvent<ContainerReflection>) => {
return reflectionMemberTemplate(pageEvent, {
allowProtoMethodsByDefault: true,
allowProtoPropertiesByDefault: true,
data: { theme: this },
})
}
}
getIndexTemplate() {
return (pageEvent: PageEvent<ContainerReflection>) => {
return indexTemplate(pageEvent, {
allowProtoMethodsByDefault: true,
allowProtoPropertiesByDefault: true,
data: { theme: this },
})
}
}
getNavigation(project: ProjectReflection) {
const urls = this.getUrls(project)
const getUrlMapping = (name: string) => {
if (!name) {
return ""
}
return urls.find((url) => url.model.name === name)
}
const createNavigationItem = (
title: string,
url: string | undefined,
isLabel: boolean,
children: NavigationItem[] = []
) => {
const navigationItem = new NavigationItem(title, url)
navigationItem.isLabel = isLabel
navigationItem.children = children
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { reflection, parent, ...filteredNavigationItem } = navigationItem
return filteredNavigationItem as NavigationItem
}
const navigation = createNavigationItem(project.name, undefined, false)
const hasReadme = !this.readme.endsWith("none")
if (hasReadme) {
navigation.children?.push(
createNavigationItem("Readme", this.entryDocument, false)
)
}
if (this.entryPoints.length === 1) {
navigation.children?.push(
createNavigationItem(
"Exports",
hasReadme ? this.globalsFile : this.entryDocument,
false
)
)
}
this.mappings.forEach((mapping) => {
const kind = mapping.kind[0]
const items = project.getReflectionsByKind(kind)
if (items.length > 0) {
const children = items
.map((item) => {
const urlMapping = getUrlMapping(item.name) || ""
return createNavigationItem(
item.getFullName(),
typeof urlMapping === "string" ? urlMapping : urlMapping.url,
true
)
})
.sort((a, b) => {
return a.title > b.title ? 1 : -1
})
const group = createNavigationItem(
getKindPlural(kind),
undefined,
true,
children
)
navigation.children?.push(group)
}
})
return navigation
}
get mappings(): Mapping[] {
return [
{
kind: [ReflectionKind.Module],
isLeaf: false,
directory: "modules",
template: this.getReflectionTemplate(),
},
{
kind: [ReflectionKind.Namespace],
isLeaf: false,
directory: "modules",
template: this.getReflectionTemplate(),
},
{
kind: [ReflectionKind.Enum],
isLeaf: false,
directory: "enums",
template: this.getReflectionTemplate(),
},
{
kind: [ReflectionKind.Class],
isLeaf: false,
directory: "classes",
template: this.getReflectionTemplate(),
},
{
kind: [ReflectionKind.Interface],
isLeaf: false,
directory: "interfaces",
template: this.getReflectionTemplate(),
},
...(this.allReflectionsHaveOwnDocument
? [
{
kind: [ReflectionKind.TypeAlias],
isLeaf: true,
directory: "types",
template: this.getReflectionMemberTemplate(),
},
{
kind: [ReflectionKind.Variable],
isLeaf: true,
directory: "variables",
template: this.getReflectionMemberTemplate(),
},
{
kind: [ReflectionKind.Function],
isLeaf: true,
directory: "functions",
template: this.getReflectionMemberTemplate(),
},
]
: []),
]
}
/**
* Triggered before the renderer starts rendering a project.
*
* @param event An event object describing the current render operation.
*/
protected onBeginRenderer(event: RendererEvent) {
this.project = event.project
}
/**
* Triggered before a document will be rendered.
*
* @param page An event object describing the current render operation.
*/
protected onBeginPage(page: PageEvent) {
this.location = page.url
this.reflection =
page.model instanceof DeclarationReflection ? page.model : undefined
const options = this.getFormattingOptionsForLocation()
if (this.reflection && this.reflection.groups) {
// filter out unwanted groups
const tempGroups: ReflectionGroup[] = []
this.reflection.groups.forEach((reflectionGroup) => {
if (
!options.reflectionGroups ||
!(reflectionGroup.title in options.reflectionGroups) ||
options.reflectionGroups[reflectionGroup.title]
) {
tempGroups.push(reflectionGroup)
}
})
this.reflection.groups = tempGroups
}
}
get globalsFile() {
return "modules.md"
}
getFormattingOptionsForLocation(): FormattingOptionType {
if (!this.location) {
return {}
}
const optionKey =
Object.keys(this.formattingOptions).find((key) => {
if (key === "*") {
return false
}
const keyPattern = new RegExp(key)
if (keyPattern.test(this.location)) {
return true
}
}) || "*"
return optionKey in this.formattingOptions
? this.formattingOptions[optionKey]
: {}
}
}
@@ -0,0 +1,143 @@
import {
ContainerReflection,
DeclarationReflection,
PageEvent,
ParameterReflection,
Reflection,
ReflectionKind,
TypeParameterReflection,
} from "typedoc"
export type ParameterStyle = "table" | "list"
export type ReflectionTitleOptions = {
typeParameters?: boolean
kind?: boolean
}
export type ObjectLiteralDeclarationStyle = "table" | "list"
export type SectionKey =
| "comment"
| "member_declaration_typeParameters"
| "member_declaration_indexSignature"
| "member_declaration_signatures"
| "member_declaration_typeDeclaration"
| "member_getteSetter_getSignature"
| "member_getteSetter_setSignature"
| "member_signatures"
| "member_getterSetter"
| "member_reference"
| "member_declaration"
| "member_signature_title"
| "member_signature_comment"
| "member_signature_typeParameters"
| "member_signature_parameters"
| "showReturnSignature"
| "member_signature_declarationSignatures"
| "member_signature_declarationChildren"
| "member_signature_comment"
| "member_signature_sources"
| "member_sources_implementationOf"
| "member_sources_inheritedFrom"
| "member_sources_overrides"
| "member_sources_definedIn"
| "members_group_categories"
| "members_categories"
| "title_reflectionPath"
| "reflection_comment"
| "reflection_typeParameters"
| "reflection_hierarchy"
| "reflection_implements"
| "reflection_implementedBy"
| "reflection_callable"
| "reflection_indexable"
export type FormattingOptionType = {
sections?: {
[k in SectionKey]: boolean
}
reflectionGroups?: {
[k: string]: boolean
}
reflectionTitle?: {
kind: boolean
typeParameters: boolean
suffix?: string
}
reflectionDescription?: string
expandMembers?: boolean
showCommentsAsHeader?: boolean
parameterStyle?: ParameterStyle
showReturnSignature?: boolean
}
export type FormattingOptionsType = {
[k: string]: FormattingOptionType
}
export type ReflectionParameterType =
| ParameterReflection
| DeclarationReflection
| TypeParameterReflection
export type Mapping = {
kind: ReflectionKind[]
isLeaf: boolean
directory: string
template: (pageEvent: PageEvent<ContainerReflection>) => string
}
export class NavigationItem {
title: string
url: string
dedicatedUrls?: string[]
parent?: NavigationItem
children?: NavigationItem[]
isLabel?: boolean
isVisible?: boolean
isCurrent?: boolean
isModules?: boolean
isInPath?: boolean
reflection?: Reflection
constructor(
title?: string,
url?: string,
parent?: NavigationItem,
reflection?: Reflection
) {
this.title = title || ""
this.url = url || ""
this.parent = parent
this.reflection = reflection
if (!url) {
this.isLabel = true
}
if (this.parent) {
if (!this.parent.children) {
this.parent.children = []
}
this.parent.children.push(this)
}
}
static create(
reflection: Reflection,
parent?: NavigationItem,
useShortNames?: boolean
) {
let name: string
if (useShortNames || (parent && parent.parent)) {
name = reflection.name
} else {
name = reflection.getFullName()
}
name = name.trim()
return new NavigationItem(name, reflection.url, parent, reflection)
}
}
@@ -0,0 +1,83 @@
import {
DeclarationReflection,
ParameterReflection,
ProjectReflection,
Reflection,
ReflectionKind,
SignatureReflection,
} from "typedoc"
export function formatContents(contents: string) {
return (
contents
.replace(/[\r\n]{3,}/g, "\n\n")
.replace(/!spaces/g, "")
.replace(/^\s+|\s+$/g, "") + "\n"
)
}
export function escapeChars(str: string) {
return str
.replace(/>/g, "\\>")
.replace(/_/g, "\\_")
.replace(/`/g, "\\`")
.replace(/\|/g, "\\|")
}
export function memberSymbol(
reflection: DeclarationReflection | ParameterReflection | SignatureReflection
) {
const isStatic = reflection.flags && reflection.flags.isStatic
if (reflection.kind === ReflectionKind.CallSignature) {
return "▸"
}
if (reflection.kind === ReflectionKind.TypeAlias) {
return "Ƭ"
}
if (reflection.kind === ReflectionKind.Property && isStatic) {
return "▪"
}
return "•"
}
export function spaces(length: number) {
return `!spaces${[...Array(length)].map(() => " ").join("")}`
}
export function stripComments(str: string) {
return str
.replace(/(?:\/\*(?:[\s\S]*?)\*\/)|(?:^\s*\/\/(?:.*)$)/g, " ")
.replace(/\n/g, "")
.replace(/^\s+|\s+$|(\s)+/g, "$1")
}
export function stripLineBreaks(str: string) {
return str
? str
.replace(/\n/g, " ")
.replace(/\r/g, " ")
.replace(/\t/g, " ")
.replace(/[\s]{2,}/g, " ")
.trim()
: ""
}
export function camelToTitleCase(text: string) {
return (
text.substring(0, 1).toUpperCase() +
text.substring(1).replace(/[a-z][A-Z]/g, (x) => `${x[0]} ${x[1]}`)
)
}
export function getDisplayName(refl: Reflection): string {
let version = ""
if (
(refl instanceof DeclarationReflection ||
refl instanceof ProjectReflection) &&
refl.packageVersion
) {
version = ` - v${refl.packageVersion}`
}
return `${refl.name}${version}`
}
@@ -0,0 +1,46 @@
import * as Handlebars from "handlebars"
import { PageEvent } from "typedoc/dist/lib/output/events"
export interface FrontMatterVars {
[key: string]: string | number | boolean
}
/**
* Prepends YAML block to a string
* @param contents - the string to prepend
* @param vars - object of required front matter variables
*/
export const prependYAML = (contents: string, vars: FrontMatterVars) => {
return contents
.replace(/^/, toYAML(vars) + "\n\n")
.replace(/[\r\n]{3,}/g, "\n\n")
}
/**
* Returns the page title as rendered in the document h1(# title)
* @param page
*/
export const getPageTitle = (page: PageEvent) => {
return Handlebars.helpers.reflectionTitle.call(page, false)
}
/**
* Converts YAML object to a YAML string
* @param vars
*/
const toYAML = (vars: FrontMatterVars) => {
const yaml = `---
${Object.entries(vars)
.map(
([key, value]) =>
`${key}: ${
typeof value === "string" ? `"${escapeString(value)}"` : value
}`
)
.join("\n")}
---`
return yaml
}
// prettier-ignore
const escapeString = (str: string) => str.replace(/"/g, "\\\"")
@@ -0,0 +1,36 @@
import { ReflectionKind } from "typedoc"
import { ReflectionParameterType } from "../types"
export function flattenParams(
current: ReflectionParameterType
): ReflectionParameterType[] {
if (!current.type || !("declaration" in current.type)) {
return []
}
return (
current.type?.declaration?.children?.reduce(
(acc: ReflectionParameterType[], child: ReflectionParameterType) => {
const childObj = {
...child,
name: `${current.name}.${child.name}`,
} as ReflectionParameterType
return parseParams(childObj, acc)
},
[]
) || []
)
}
export function parseParams(
current: ReflectionParameterType,
acc: ReflectionParameterType[]
): ReflectionParameterType[] {
const shouldFlatten =
current.type &&
"declaration" in current.type &&
current.type?.declaration?.kind === ReflectionKind.TypeLiteral &&
current.type?.declaration?.children
return shouldFlatten
? [...acc, current, ...flattenParams(current)]
: [...acc, current]
}
@@ -0,0 +1,70 @@
import { Comment, DeclarationReflection, ReflectionType } from "typedoc"
import * as Handlebars from "handlebars"
import { stripLineBreaks } from "../utils"
import { ReflectionParameterType } from "../types"
const MAX_LEVEL = 3
export default function reflectionFomatter(
reflection: ReflectionParameterType,
level = 1
) {
const prefix = `${Array(level - 1)
.fill("\t")
.join("")}-`
let item = `${prefix} \`${reflection.name}\`: `
const defaultValue = getDefaultValue(reflection)
if (defaultValue || reflection.flags.isOptional) {
item += `(${reflection.flags.isOptional ? "optional" : ""}${
reflection.flags.isOptional && defaultValue ? "," : ""
}${defaultValue ? `default: ${defaultValue}` : ""}) `
}
const comments = getComments(reflection)
if (comments) {
item += stripLineBreaks(Handlebars.helpers.comments(comments))
const itemChildren: string[] = []
comments.summary.forEach((commentSummary) => {
if ("target" in commentSummary) {
const reflection = commentSummary.target as DeclarationReflection
if (reflection.children && level + 1 < MAX_LEVEL) {
reflection.children.forEach((childItem) => {
itemChildren.push(reflectionFomatter(childItem, level + 1))
})
}
}
})
if (itemChildren.length) {
// TODO maybe we should check the type of the reflection and replace
// `properties` with the text that makes sense for the type.
item += ` It accepts the following properties:\n${itemChildren.join(
"\n"
)}`
}
}
return item
}
function getDefaultValue(parameter: ReflectionParameterType): string | null {
if (!("defaultValue" in parameter)) {
return null
}
return parameter.defaultValue && parameter.defaultValue !== "..."
? `\`${parameter.defaultValue}\``
: null
}
function getComments(parameter: ReflectionParameterType): Comment | undefined {
if (parameter.type instanceof ReflectionType) {
if (
parameter.type?.declaration?.signatures &&
parameter.type?.declaration?.signatures[0]?.comment
) {
return parameter.type?.declaration?.signatures[0]?.comment
}
}
return parameter.comment
}
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src"]
}
@@ -0,0 +1,2 @@
dist
.yarn
@@ -0,0 +1,15 @@
# typedoc-plugin-modules
A Typedoc plugin that includes helper plugins for documenting modules. The `resolve-reference-plugin` imitates the [`typedoc-plugin-missing-exports`](https://www.npmjs.com/package/typedoc-plugin-missing-exports) plugin.
## Configurations
Accepts the same options as the [`typedoc-plugin-missing-exports`](https://www.npmjs.com/package/typedoc-plugin-missing-exports) plugin.
## Build the Plugin
Before using any command that makes use of this plugin, make sure to run the `build` command:
```bash
yarn build
```
@@ -0,0 +1,38 @@
{
"name": "typedoc-plugin-modules",
"private": true,
"version": "0.0.0",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"description": "A plugin that holds all necessary changes to generate a module reference",
"main": "./dist/index.js",
"exports": "./dist/index.js",
"files": [
"dist"
],
"author": "Shahed Nasser",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"lint": "eslint --ext .ts src"
},
"peerDependencies": {
"typedoc": "0.25.x"
},
"devDependencies": {
"@types/node": "^16.11.10",
"typescript": "^4.6"
},
"keywords": [
"typedocplugin",
"packages",
"monorepo",
"typedoc"
],
"dependencies": {
"glob": "^10.3.10",
"typedoc-plugin-markdown": "^3.16.0"
}
}
@@ -0,0 +1,6 @@
import { Application } from "typedoc"
import { load as resolveReferencesPluginLoad } from "./resolve-references-plugin"
export function load(app: Application) {
resolveReferencesPluginLoad(app)
}
@@ -0,0 +1,184 @@
import { ok } from "assert"
import {
Application,
Context,
Converter,
ReflectionKind,
TypeScript as ts,
ReferenceType,
Reflection,
DeclarationReflection,
ProjectReflection,
} from "typedoc"
declare module "typedoc" {
export interface TypeDocOptionMap {
internalModule: string
}
}
let hasMonkeyPatched = false
export function load(app: Application) {
if (hasMonkeyPatched) {
throw new Error(
"typedoc-plugin-missing-exports cannot be loaded multiple times"
)
}
hasMonkeyPatched = true
let activeReflection: Reflection | undefined
const referencedSymbols = new Map<ts.Program, Set<ts.Symbol>>()
const symbolToActiveRefl = new Map<ts.Symbol, Reflection>()
const knownPrograms = new Map<Reflection, ts.Program>()
function discoverMissingExports(
context: Context,
program: ts.Program
): Set<ts.Symbol> {
// An export is missing if if was referenced
// Is not contained in the documented
// And is "owned" by the active reflection
const referenced = referencedSymbols.get(program) || new Set()
const ownedByOther = new Set<ts.Symbol>()
referencedSymbols.set(program, ownedByOther)
for (const s of [...referenced]) {
if (context.project.getReflectionFromSymbol(s)) {
referenced.delete(s)
} else if (symbolToActiveRefl.get(s) !== activeReflection) {
referenced.delete(s)
ownedByOther.add(s)
}
}
return referenced
}
// Monkey patch the constructor for references so that we can get every
const origCreateSymbolReference = ReferenceType.createSymbolReference
ReferenceType.createSymbolReference = function (symbol, context, name) {
ok(activeReflection, "active reflection has not been set")
const set = referencedSymbols.get(context.program)
symbolToActiveRefl.set(symbol, activeReflection)
if (set) {
set.add(symbol)
} else {
referencedSymbols.set(context.program, new Set([symbol]))
}
return origCreateSymbolReference.call(this, symbol, context, name)
}
app.options.addDeclaration({
name: "internalModule",
help: "Define the name of the module that internal symbols which are not exported should be placed into.",
})
app.converter.on(
Converter.EVENT_CREATE_DECLARATION,
(context: Context, refl: Reflection) => {
if (refl.kindOf(ReflectionKind.Project | ReflectionKind.Module)) {
knownPrograms.set(refl, context.program)
activeReflection = refl
}
}
)
app.converter.on(
Converter.EVENT_RESOLVE_BEGIN,
function onResolveBegin(context: Context) {
const modules: (DeclarationReflection | ProjectReflection)[] =
context.project.getChildrenByKind(ReflectionKind.Module)
if (modules.length === 0) {
// Single entry point, just target the project.
modules.push(context.project)
}
for (const mod of modules) {
activeReflection = mod
const program = knownPrograms.get(mod)
if (!program) {
continue
}
let missing = discoverMissingExports(context, program)
if (!missing || !missing.size) {
continue
}
// Nasty hack here that will almost certainly break in future TypeDoc versions.
context.setActiveProgram(program)
const internalModuleOption =
context.converter.application.options.getValue("internalModule")
let internalNs: DeclarationReflection | undefined = undefined
if (internalModuleOption) {
internalNs = context
.withScope(mod)
.createDeclarationReflection(
ReflectionKind.Module,
void 0,
void 0,
context.converter.application.options.getValue("internalModule")
)
context.finalizeDeclarationReflection(internalNs)
}
const internalContext = context.withScope(internalNs || mod)
// Keep track of which symbols we've tried to convert. If they don't get converted
// when calling convertSymbol, then the user has excluded them somehow, don't go into
// an infinite loop when converting.
const tried = new Set<ts.Symbol>()
do {
for (const s of missing) {
if (shouldConvertSymbol(s, context.checker)) {
internalContext.converter.convertSymbol(internalContext, s)
}
tried.add(s)
}
missing = discoverMissingExports(context, program)
for (const s of tried) {
missing.delete(s)
}
} while (missing.size > 0)
// All the missing symbols were excluded, so get rid of our namespace.
if (internalNs && !internalNs.children?.length) {
context.project.removeReflection(internalNs)
}
context.setActiveProgram(void 0)
}
knownPrograms.clear()
referencedSymbols.clear()
symbolToActiveRefl.clear()
},
void 0,
1e9
)
}
function shouldConvertSymbol(symbol: ts.Symbol, checker: ts.TypeChecker) {
while (symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(symbol)
}
// We're looking at an unknown symbol which is declared in some package without
// type declarations. We know nothing about it, so don't convert it.
if (symbol.flags & ts.SymbolFlags.Transient) {
return false
}
// This is something inside the special Node `Globals` interface. Don't convert it
// because TypeDoc will reasonably assert that "Property" means that a symbol should be
// inside something that can have properties.
if (symbol.flags & ts.SymbolFlags.Property && symbol.name !== "default") {
return false
}
return true
}
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src"]
}