diff --git a/devtools/packages/prebuild-options/tasks/generate-models.ts b/devtools/packages/prebuild-options/tasks/generate-models.ts index 87ac5bc31..caf56fb0b 100644 --- a/devtools/packages/prebuild-options/tasks/generate-models.ts +++ b/devtools/packages/prebuild-options/tasks/generate-models.ts @@ -5,11 +5,7 @@ import * as path from 'path'; import * as prettier from 'prettier'; import { DeclarationOption, ParameterType } from 'typedoc'; -const ignoreTypes = [ - 'textContentMappings', - 'remarkPlugins', - 'remarkStringifyOptions', -]; +const ignoreTypes = ['textContentMappings', 'remarkStringifyOptions']; export async function generateOptionsModels(docsConfig: DocsConfig) { const optionsConfig = await import(docsConfig.declarationsPath as string); @@ -34,7 +30,7 @@ async function writeTypeDocDeclarations( (option as any).type === ParameterType.Mixed && (option as any).defaultValue, ) - .map(([name, option]) => capitalize(name)); + .map(([name, option]) => capitalize(name, false)); const out: string[] = []; @@ -126,17 +122,20 @@ ${name}: ${getType(name, option, true)};`, ?.filter(([name]) => !ignoreTypes.includes(name)) .map(([name, option]) => { return ` - /** - * ${getComments(name)} - */ - export interface ${capitalize(name)} { + ${ + // this is a hack need to fix properly + name === 'remarkPlugins' + ? 'export type RemarkPlugin = string | [string, Record];' + : `export interface ${capitalize(name)} { ${Object.entries(option.defaultValue as any) .map( ([key, value]) => `'${key}'${value === undefined ? '?' : ''}: ${getValueType(key, value)}`, ) .join(';')} - } + }` + } + `; }) .join('\n')} @@ -255,6 +254,9 @@ function getObjectType(name: string) { return 'string'; } -function capitalize(str: string) { +function capitalize(str: string, includeArray = true) { + if (str.endsWith('s')) { + str = `${str.slice(0, -1)}${includeArray ? '[]' : ''}`; + } return str.charAt(0).toUpperCase() + str.slice(1); } diff --git a/package-lock.json b/package-lock.json index 1b8556d2b..de1d9a697 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23197,7 +23197,7 @@ } }, "packages/typedoc-plugin-markdown": { - "version": "4.3.2", + "version": "4.3.3", "license": "MIT", "engines": { "node": ">= 18" @@ -23214,7 +23214,8 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-mdx": "^3.1.0", - "to-vfile": "^8.0.0" + "to-vfile": "^8.0.0", + "unist-util-visit": "^5.0.0" }, "peerDependencies": { "typedoc-plugin-markdown": ">=4.3.0" diff --git a/packages/typedoc-plugin-remark/package.json b/packages/typedoc-plugin-remark/package.json index d332043b4..5470ae564 100644 --- a/packages/typedoc-plugin-remark/package.json +++ b/packages/typedoc-plugin-remark/package.json @@ -14,6 +14,7 @@ "prepublishOnly": "npm run lint && npm run build", "prebuild": "rm -rf dist && prebuild-options", "build": "tsc", + "build-and-run": "npm run build && npm run pretest", "pretest": "tsx ./test/__scripts__/prepare.ts", "test": "jest", "test:update": "npm run build && npm run test -- -u" @@ -31,7 +32,8 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-mdx": "^3.1.0", - "to-vfile": "^8.0.0" + "to-vfile": "^8.0.0", + "unist-util-visit": "^5.0.0" }, "peerDependencies": { "typedoc-plugin-markdown": ">=4.3.0" diff --git a/packages/typedoc-plugin-remark/src/_typedoc.d.ts b/packages/typedoc-plugin-remark/src/_typedoc.d.ts index 4263970b4..611c0699b 100644 --- a/packages/typedoc-plugin-remark/src/_typedoc.d.ts +++ b/packages/typedoc-plugin-remark/src/_typedoc.d.ts @@ -1,9 +1,10 @@ // THIS FILE IS AUTO GENERATED FROM THE OPTIONS CONFIG. DO NOT EDIT DIRECTLY. import { ManuallyValidatedOption } from 'typedoc'; +import { RemarkPlugin } from './types/options.js'; declare module 'typedoc' { export interface TypeDocOptionMap { defaultRemarkPlugins: { gfm: boolean; frontmatter: boolean; mdx: boolean }; - remarkPlugins: ManuallyValidatedOption>; + remarkPlugins: ManuallyValidatedOption; remarkStringifyOptions: ManuallyValidatedOption>; } } diff --git a/packages/typedoc-plugin-remark/src/helpers/add-toc.ts b/packages/typedoc-plugin-remark/src/helpers/add-toc.ts deleted file mode 100644 index 6b81775ba..000000000 --- a/packages/typedoc-plugin-remark/src/helpers/add-toc.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Application, DeclarationReflection, ReflectionKind } from 'typedoc'; -import { RemarkPlugin } from '../types/remark-plugin.js'; - -export function addTableOfContents( - event: any, - remarkPlugins: RemarkPlugin[], - remarkPluginsNames: string[], - app: Application, -) { - const tocPluginIndex = remarkPluginsNames.findIndex( - (name) => name === 'remark-toc', - ); - const tocPlugin = remarkPlugins[tocPluginIndex]; - const options = Array.isArray(tocPlugin) ? tocPlugin[1] : {}; - - const isModulesOnly = (event?.model as DeclarationReflection).children?.every( - (child) => child.kind === ReflectionKind.Module, - ); - const outputFileStrategy = app.options.getValue('outputFileStrategy'); - - const kindsWithToc = [ - ReflectionKind.Project, - ReflectionKind.Module, - ReflectionKind.Namespace, - ReflectionKind.Class, - ReflectionKind.Enum, - ReflectionKind.Interface, - ReflectionKind.Document, - ]; - - if (outputFileStrategy === 'modules') { - kindsWithToc.push(ReflectionKind.Module); - } - - if (!isModulesOnly) { - kindsWithToc.push(ReflectionKind.Project); - } - - if (kindsWithToc.includes(event.model?.kind)) { - const contents = event.contents; - const contentToLines = contents?.split('\n'); - const numberOfLinesStartingWithHash = contentToLines?.filter((line) => - line.startsWith('## '), - ).length; - if (numberOfLinesStartingWithHash > 1) { - const firstHeadingIndex = contentToLines?.findIndex((line) => - line.startsWith('## '), - ); - if (firstHeadingIndex && firstHeadingIndex > 0) { - contentToLines?.splice( - firstHeadingIndex, - 0, - `\n\n## ${(options as any)?.heading || 'Contents'}\n\n`, - ); - event.contents = contentToLines?.join('\n'); - } - } - } -} diff --git a/packages/typedoc-plugin-remark/src/helpers/get-default-plugins.ts b/packages/typedoc-plugin-remark/src/helpers/get-default-plugins.ts index cd7623334..df3d0bce9 100644 --- a/packages/typedoc-plugin-remark/src/helpers/get-default-plugins.ts +++ b/packages/typedoc-plugin-remark/src/helpers/get-default-plugins.ts @@ -1,4 +1,4 @@ -import { RemarkPlugin } from '../types/remark-plugin.js'; +import { RemarkPlugin } from '../types/options.js'; export function getDefaultPlugins(defaultPluginsFlag: { gfm: boolean; diff --git a/packages/typedoc-plugin-remark/src/index.ts b/packages/typedoc-plugin-remark/src/index.ts index ef9693af3..3da605c45 100644 --- a/packages/typedoc-plugin-remark/src/index.ts +++ b/packages/typedoc-plugin-remark/src/index.ts @@ -4,13 +4,16 @@ * @module */ +import * as path from 'path'; import { Application, DeclarationOption, RendererEvent } from 'typedoc'; -import { MarkdownPageEvent } from 'typedoc-plugin-markdown'; -import { addTableOfContents } from './helpers/add-toc.js'; +import { fileURLToPath } from 'url'; import { getDefaultPlugins } from './helpers/get-default-plugins.js'; import * as options from './options/declarations.js'; import { parse } from './parse.js'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + export function load(app: Application) { Object.entries(options).forEach(([name, option]) => { app.options.addDeclaration({ @@ -19,18 +22,8 @@ export function load(app: Application) { } as DeclarationOption); }); - app.renderer.on(MarkdownPageEvent.END, (event: MarkdownPageEvent) => { - const remarkPlugins = app.options.getValue('remarkPlugins') as []; - const remarkPluginsNames = remarkPlugins.map((plugin) => - Array.isArray(plugin) ? plugin[0] : plugin, - ) as string[]; - - if (remarkPluginsNames.includes('remark-toc')) { - addTableOfContents(event, remarkPlugins, remarkPluginsNames, app); - } - }); - app.renderer.postRenderAsyncJobs.push(async (output: RendererEvent) => { + const remarkPlugins = app.options.getValue('remarkPlugins') as []; const defaultPlugins = getDefaultPlugins( app.options.getValue('defaultRemarkPlugins'), ); @@ -38,9 +31,28 @@ export function load(app: Application) { const remarkStringifyOptions = app.options.getValue( 'remarkStringifyOptions', ); + const remarkPluginsNames = remarkPlugins.map((plugin) => + Array.isArray(plugin) ? plugin[0] : plugin, + ) as string[]; + if (output.urls?.length) { await Promise.all( output.urls?.map(async (urlMapping) => { + if (remarkPluginsNames.includes('remark-toc')) { + const tocPluginIndex = remarkPluginsNames.findIndex( + (name) => name === 'remark-toc', + ); + const tocPlugin = remarkPlugins[tocPluginIndex]; + const tocOptions = Array.isArray(tocPlugin) ? tocPlugin[1] : {}; + defaultPlugins.push([ + path.join(__dirname, 'plugins', 'add-toc.js'), + { + reflection: urlMapping.model, + typedocOptions: app.options, + tocOptions, + }, + ]); + } const filePath = `${output.outputDirectory}/${urlMapping.url}`; return await parse( filePath, diff --git a/packages/typedoc-plugin-remark/src/options/index.ts b/packages/typedoc-plugin-remark/src/options/index.ts index 0435c683a..79541bf90 100644 --- a/packages/typedoc-plugin-remark/src/options/index.ts +++ b/packages/typedoc-plugin-remark/src/options/index.ts @@ -4,5 +4,4 @@ * @module */ -export * as helpers from '../helpers/add-toc.js'; export * as declarations from './declarations.js'; diff --git a/packages/typedoc-plugin-remark/src/parse.ts b/packages/typedoc-plugin-remark/src/parse.ts index b60c99619..6d9cc8529 100644 --- a/packages/typedoc-plugin-remark/src/parse.ts +++ b/packages/typedoc-plugin-remark/src/parse.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { remark } from 'remark'; import { read, writeSync } from 'to-vfile'; -import { RemarkPlugin } from './types/remark-plugin.js'; +import { RemarkPlugin } from './types/options.js'; export async function parse( filePath: string, diff --git a/packages/typedoc-plugin-remark/src/plugins/add-toc.ts b/packages/typedoc-plugin-remark/src/plugins/add-toc.ts new file mode 100644 index 000000000..6033a147c --- /dev/null +++ b/packages/typedoc-plugin-remark/src/plugins/add-toc.ts @@ -0,0 +1,61 @@ +import { Heading } from 'mdast'; +import { DeclarationReflection, Options, ReflectionKind } from 'typedoc'; +import { Node } from 'unist'; +import { visit } from 'unist-util-visit'; + +export default function (options: any) { + const { reflection, typedocOptions, tocOptions } = options; + + return (tree: Node) => { + if (isIndexPage(reflection, typedocOptions)) { + return tree; + } + visit( + tree, + 'heading', + (node: Heading, index, parent: { children: Node[] }) => { + if (node.depth === 2) { + const newHeading: Heading = { + type: 'heading', + depth: 2, + children: [ + { + type: 'text', + value: tocOptions?.heading || 'Contents', + }, + ], + }; + parent?.children.splice(index, 0, newHeading); + return false; + } + }, + ); + }; +} + +function isIndexPage( + reflection: DeclarationReflection, + typedocOptions: Options, +) { + /** + * If reflection is a project and all children are modules, don't add TOC + */ + if ( + reflection.kind === ReflectionKind.Project && + reflection.children?.every((child) => child.kind === ReflectionKind.Module) + ) { + return true; + } + + /** + * If reflection is a module and outputFileStrategy is members the page has an index. + */ + if ( + [ReflectionKind.Project, ReflectionKind.Module].includes(reflection.kind) && + typedocOptions?.getValue('outputFileStrategy') === 'members' + ) { + return true; + } + + return false; +} diff --git a/packages/typedoc-plugin-remark/src/types/options.ts b/packages/typedoc-plugin-remark/src/types/options.ts index 326e6c8ed..7871646b2 100644 --- a/packages/typedoc-plugin-remark/src/types/options.ts +++ b/packages/typedoc-plugin-remark/src/types/options.ts @@ -13,10 +13,12 @@ export interface PluginOptions { /** * An array of remark plugin names to be executed. */ - remarkPlugins: any; + remarkPlugins: RemarkPlugin[]; /** * Custom options for the remark-stringify plugin. */ remarkStringifyOptions: Record; } + +export type RemarkPlugin = string | [string, Record]; diff --git a/packages/typedoc-plugin-remark/src/types/remark-plugin.ts b/packages/typedoc-plugin-remark/src/types/remark-plugin.ts deleted file mode 100644 index d50392be7..000000000 --- a/packages/typedoc-plugin-remark/src/types/remark-plugin.ts +++ /dev/null @@ -1 +0,0 @@ -export type RemarkPlugin = string | (string | string[])[]; diff --git a/packages/typedoc-plugin-remark/test/__scripts__/prepare.ts b/packages/typedoc-plugin-remark/test/__scripts__/prepare.ts index 246abe252..fc6921fed 100644 --- a/packages/typedoc-plugin-remark/test/__scripts__/prepare.ts +++ b/packages/typedoc-plugin-remark/test/__scripts__/prepare.ts @@ -12,9 +12,10 @@ fs.removeSync(`./test/out`); const fixtures = [ { options: 'typedoc.modules.json', outDir: 'modules' }, { options: 'typedoc.members.json', outDir: 'members' }, + { options: 'typedoc.toc.json', outDir: 'toc' }, { options: 'typedoc.globals.json', outDir: 'globals' }, - { options: 'typedoc.globals-notoc.json', outDir: 'globals-notoc' }, { options: 'typedoc.globals-mdx.json', outDir: 'globals-mdx' }, + { options: 'typedoc.globals-notoc.json', outDir: 'globals-notoc' }, ]; // write fixtures diff --git a/packages/typedoc-plugin-remark/test/stubs/module-toc.ts b/packages/typedoc-plugin-remark/test/stubs/module-toc.ts new file mode 100644 index 000000000..e989165f2 --- /dev/null +++ b/packages/typedoc-plugin-remark/test/stubs/module-toc.ts @@ -0,0 +1,31 @@ +/** + * @module + */ + +export const someVariable = true; + +export const someFunction = (a: string, b: number) => { + return true; +}; + +export class Class { + a: string; + b: number; +} + +export type SimpleTypeAlias = string | boolean; + +export type TypeAliasWithTypeDeclaration = { + a: string; + b: number; +}; + +export interface Interface { + a: string; + b: number; +} + +export enum Enum { + A, + B, +} diff --git a/packages/typedoc-plugin-remark/test/typedoc.base.json b/packages/typedoc-plugin-remark/test/typedoc.base.json index e5ba50958..4b2606cca 100644 --- a/packages/typedoc-plugin-remark/test/typedoc.base.json +++ b/packages/typedoc-plugin-remark/test/typedoc.base.json @@ -1,5 +1,5 @@ { - "entryPoints": ["./stubs/*.ts"], + "entryPoints": ["./stubs/module-1.ts", "./stubs/module-2.ts"], "tsconfig": "./stubs/tsconfig.json", "out": "./out", "plugin": [ diff --git a/packages/typedoc-plugin-remark/test/typedoc.members.json b/packages/typedoc-plugin-remark/test/typedoc.members.json index 69e3d7129..a0ee43ffb 100644 --- a/packages/typedoc-plugin-remark/test/typedoc.members.json +++ b/packages/typedoc-plugin-remark/test/typedoc.members.json @@ -1,6 +1,5 @@ { "extends": "./typedoc.base.json", - "entryPoints": ["./stubs/*.ts"], "remarkPlugins": ["remark-github", ["remark-toc", { "maxDepth": 3 }]], "defaultRemarkPlugins": { "mdx": false diff --git a/packages/typedoc-plugin-remark/test/typedoc.modules.json b/packages/typedoc-plugin-remark/test/typedoc.modules.json index df036a837..a40d0011a 100644 --- a/packages/typedoc-plugin-remark/test/typedoc.modules.json +++ b/packages/typedoc-plugin-remark/test/typedoc.modules.json @@ -1,6 +1,5 @@ { "extends": "./typedoc.base.json", - "entryPoints": ["./stubs/*.ts"], "outputFileStrategy": "modules", "remarkStringifyOptions": { "bullet": "+", "fence": "~" }, "remarkPlugins": [ diff --git a/packages/typedoc-plugin-remark/test/typedoc.toc.json b/packages/typedoc-plugin-remark/test/typedoc.toc.json new file mode 100644 index 000000000..cb6ac5fe7 --- /dev/null +++ b/packages/typedoc-plugin-remark/test/typedoc.toc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./stubs/module-toc.ts"], + "tsconfig": "./stubs/tsconfig.json", + "plugin": ["typedoc-plugin-markdown", "typedoc-plugin-remark"], + "remarkPlugins": [["remark-toc", { "maxDepth": 3 }]], + "readme": "none" +}