From 3245ede564a2c4745d3d73690cfe8bca9c9d4e36 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Wed, 30 Nov 2022 17:58:37 -0800 Subject: [PATCH] chore(typedoc-plugin-appium): a load of cleanup --- package-lock.json | 1 + .../lib/converter/builder.ts | 197 +++++++++--------- .../lib/converter/converter.ts | 38 +++- .../lib/converter/types.ts | 85 +++++--- packages/typedoc-plugin-appium/lib/guards.ts | 107 +++++++++- packages/typedoc-plugin-appium/lib/logger.ts | 24 ++- .../lib/model/command-info.ts | 13 +- .../lib/model/reflection/command.ts | 98 ++++++--- .../lib/model/reflection/commands.ts | 46 ++-- .../lib/model/reflection/index.ts | 1 - .../lib/model/reflection/kind.ts | 16 +- .../lib/model/reflection/plugin.ts | 6 - .../lib/model/reflection/utils.ts | 1 + .../typedoc-plugin-appium/lib/model/types.ts | 8 +- .../typedoc-plugin-appium/lib/output/index.ts | 1 - .../lib/output/theme/appium.ts | 61 ------ .../lib/output/theme/index.ts | 2 - .../lib/output/theme/types.ts | 14 -- .../lib/output/theme/utils.ts | 60 ------ packages/typedoc-plugin-appium/lib/plugin.ts | 8 +- .../lib/theme/helpers.ts | 63 ++++++ .../typedoc-plugin-appium/lib/theme/index.ts | 98 +++++++++ .../lib/theme/template.ts | 64 ++++++ packages/typedoc-plugin-appium/package.json | 1 + .../resources/templates/commands.hbs | 14 +- typedoc.json | 1 + 26 files changed, 672 insertions(+), 356 deletions(-) delete mode 100644 packages/typedoc-plugin-appium/lib/model/reflection/plugin.ts delete mode 100644 packages/typedoc-plugin-appium/lib/output/index.ts delete mode 100644 packages/typedoc-plugin-appium/lib/output/theme/appium.ts delete mode 100644 packages/typedoc-plugin-appium/lib/output/theme/index.ts delete mode 100644 packages/typedoc-plugin-appium/lib/output/theme/types.ts delete mode 100644 packages/typedoc-plugin-appium/lib/output/theme/utils.ts create mode 100644 packages/typedoc-plugin-appium/lib/theme/helpers.ts create mode 100644 packages/typedoc-plugin-appium/lib/theme/index.ts create mode 100644 packages/typedoc-plugin-appium/lib/theme/template.ts diff --git a/package-lock.json b/package-lock.json index e10132248..95dfba9ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23760,6 +23760,7 @@ "license": "Apache-2.0", "dependencies": { "handlebars": "4.7.7", + "pluralize": "8.0.0", "type-fest": "3.2.0", "typedoc-plugin-markdown": "3.13.6" }, diff --git a/packages/typedoc-plugin-appium/lib/converter/builder.ts b/packages/typedoc-plugin-appium/lib/converter/builder.ts index 6226eeaea..08bfbf886 100644 --- a/packages/typedoc-plugin-appium/lib/converter/builder.ts +++ b/packages/typedoc-plugin-appium/lib/converter/builder.ts @@ -1,3 +1,9 @@ +/** + * A thing that creates {@linkcode typedoc!DeclarationReflection} instances from parsed + * command & execute method data. + * @module + */ + import {Context, ReflectionKind} from 'typedoc'; import {AppiumPluginLogger} from '../logger'; import { @@ -5,117 +11,110 @@ import { CommandInfo, CommandReflection, CommandsReflection, - ExecCommandData, + ExecMethodData, ModuleCommands, ParentReflection, Route, } from '../model'; -export class CommandTreeBuilder { - #log: AppiumPluginLogger; - - constructor(log: AppiumPluginLogger) { - this.#log = log.createChildLogger('builder'); - } - - /** - * Creates `DeclarationReflection` based on the `ModuleCommands` object & adds them to the project. - * @param ctx TypeDoc Context - * @param commands Data from the converter - */ - public createReflections(ctx: Context, commands: ModuleCommands): void { - const {project} = ctx; - const modules = project.getChildrenByKind(ReflectionKind.Module); - - // the project itself may have commands, as well as any modules within the project - const parents = [...modules, project].filter((parent) => commands.get(parent)?.hasCommands); - if (parents.length) { - for (const parent of parents) { - this.#createCommandsReflection(ctx, parent, commands.get(parent)!); - } - } else { - this.#log.warn('No Appium commands found in the entire project'); - } - } - - /** - * Creates and adds a child {@linkcode CommandReflection} to this reflection - * @param data Command reference - * @param route Route - * @param parent Commands reflection - */ - #createCommandReflection( - ctx: Context, - data: CommandData | ExecCommandData, - parent: CommandsReflection, - route?: Route - ): void { - const commandRefl = new CommandReflection(data, parent, route); - /** - * During "normal" usage of TypeDoc, one would call `createDeclarationReflection()`. But - * since we've subclassed `DeclarationReflection`, we cannot call it directly. It doesn't - * seem to do anything useful besides instantiation then delegating to `postReflectionCreation()`; - * so we just need to call it directly. - * - * Finally, we call `finalizeDeclarationReflection()` which I think just fires some events for other - * plugins to potentially use. - * - * And yes, the `undefined`s are apparently needed. - */ - ctx.postReflectionCreation(commandRefl, undefined, undefined); - ctx.finalizeDeclarationReflection(commandRefl); - } - - /** - * Create a new {@linkcode CommandsReflection} - * @param ctx - Current context - * @param parent - Parent module (or project) - * @param commandInfo - Command information for `module` - */ - #createCommandsReflection( - ctx: Context, - parent: ParentReflection, - commandInfo: CommandInfo - ): CommandsReflection { - // TODO: module.name may not be right here - const commandsRefl = new CommandsReflection(parent.name, ctx.project, commandInfo); - /** - * See note in `#createCommandReflection` above about this call - */ - ctx.postReflectionCreation(commandsRefl, undefined, undefined); - - const parentCtx = ctx.withScope(commandsRefl); - const {routeMap: routeMap, execCommandDataSet: execCommandsData} = commandInfo; - - // sort routes in alphabetical order - const sortedRouteMap = new Map([...routeMap.entries()].sort()); - for (const [route, commandMap] of sortedRouteMap) { - for (const data of commandMap.values()) { - this.#createCommandReflection(parentCtx, data, commandsRefl, route); - } - } - - // sort execute commands in alphabetical order - const sortedExecCommandsData = new Set([...execCommandsData].sort()); - for (const data of sortedExecCommandsData) { - this.#createCommandReflection(parentCtx, data, commandsRefl); - } - - ctx.finalizeDeclarationReflection(commandsRefl); - return commandsRefl; - } +/** + * Creates and adds a child {@linkcode CommandReflection} to this reflection + * + * During "normal" usage of TypeDoc, one would call + * `createDeclarationReflection()`. But since we've subclassed + * `DeclarationReflection`, we cannot call it directly. It doesn't seem to do + * anything useful besides instantiation then delegating to + * `postReflectionCreation()`; so we just need to call it directly. + * + * Finally, we call `finalizeDeclarationReflection()` which I think just fires + * some events for other plugins to potentially use. + * @param log Logger + * @param data Command reference + * @param route Route + * @param parent Commands reflection + * @internal + */ +function createCommandReflection( + log: AppiumPluginLogger, + ctx: Context, + data: CommandData | ExecMethodData, + parent: CommandsReflection, + route?: Route +): void { + const commandRefl = new CommandReflection(data, parent, route); + // yes, the `undefined`s are needed + ctx.postReflectionCreation(commandRefl, undefined, undefined); + ctx.finalizeDeclarationReflection(commandRefl); } /** - * Convenience function to instantiate a {@linkcode CommandTreeBuilder} and create relfections + * Create a new {@linkcode CommandsReflection} and all {@linkcode CommandReflection} children within it. + * @param log - Logger + * @param ctx - Current context + * @param parent - Parent module (or project) + * @param commandInfo - Command information for `module` + * @internal + */ +function createCommandsReflection( + log: AppiumPluginLogger, + ctx: Context, + parent: ParentReflection, + commandInfo: CommandInfo +): CommandsReflection { + // TODO: parent.name may not be right here + const commandsRefl = new CommandsReflection(parent.name, ctx.project, commandInfo); + /** + * See note in `#createCommandReflection` above about this call + */ + ctx.postReflectionCreation(commandsRefl, undefined, undefined); + + const parentCtx = ctx.withScope(commandsRefl); + const {routeMap: routeMap, execMethodDataSet: execCommandsData} = commandInfo; + + // sort routes in alphabetical order + const sortedRouteMap = new Map([...routeMap.entries()].sort()); + for (const [route, commandMap] of sortedRouteMap) { + for (const data of commandMap.values()) { + createCommandReflection(log, parentCtx, data, commandsRefl, route); + } + } + + // sort execute commands in alphabetical order + const sortedExecCommandsData = new Set([...execCommandsData].sort()); + for (const data of sortedExecCommandsData) { + createCommandReflection(log, parentCtx, data, commandsRefl); + } + + ctx.finalizeDeclarationReflection(commandsRefl); + return commandsRefl; +} + +/** + * Creates custom {@linkcode typedoc!DeclarationReflection}s from parsed command & execute method data. + * + * These instances are added to the {@linkcode Context} object itself; this mutates TypeDoc's internal state. Nothing is returned. * @param ctx TypeDoc Context - * @param log Plugin logger - * @param commandInfo Command info from converter + * @param parentLog Plugin logger + * @param commandInfo Command info from converter; a map of parent reflections to parsed data */ export function createReflections( ctx: Context, - log: AppiumPluginLogger, + parentLog: AppiumPluginLogger, commandInfo: ModuleCommands ): void { - new CommandTreeBuilder(log).createReflections(ctx, commandInfo); + const log = parentLog.createChildLogger('builder'); + const {project} = ctx; + + // note that this could be an empty array + const modules = project.getChildrenByKind(ReflectionKind.Module); + + // the project itself may have commands, as well as any modules within the project + const parents = [...modules, project].filter((parent) => commandInfo.get(parent)?.hasData); + if (!parents.length) { + log.warn('No Appium commands found in the entire project'); + // TODO: maybe we should abort processing gracefully here? or throw? + } + for (const parent of parents) { + createCommandsReflection(log, ctx, parent, commandInfo.get(parent)!); + } } diff --git a/packages/typedoc-plugin-appium/lib/converter/converter.ts b/packages/typedoc-plugin-appium/lib/converter/converter.ts index 3f36bac09..e38a5bcb3 100644 --- a/packages/typedoc-plugin-appium/lib/converter/converter.ts +++ b/packages/typedoc-plugin-appium/lib/converter/converter.ts @@ -1,3 +1,19 @@ +/** + * Converts code parsed by TypeDoc into a data structure describing the commands and execute methods, which will later be used to create new {@linkcode DeclarationReflection} instances in the TypeDoc context. + * + * The logic in this module is highly dependent on Appium's extension API, and is further dependent on specific usages of TS types. Anything that will be parsed successfully by this module must use a `const` type alias in TS parlance. For example: + * + * ```ts + * const METHOD_MAP = { + * '/status': { + * GET: {command: 'getStatus'} + * }, + * // ... + * } as const; // <-- required + * ``` + * @module + */ + import _ from 'lodash'; import {Context, DeclarationReflection, LiteralType, ReflectionKind} from 'typedoc'; import { @@ -14,7 +30,7 @@ import {AppiumPluginLogger} from '../logger'; import { CommandInfo, CommandMap, - ExecCommandDataSet, + ExecMethodDataSet, ModuleCommands, ParentReflection, RouteMap, @@ -29,6 +45,7 @@ import { * Name of the static `newMethodMap` property in a Driver */ export const NAME_NEW_METHOD_MAP = 'newMethodMap'; + /** * Name of the static `executeMethodMap` property in a Driver */ @@ -70,7 +87,14 @@ export const NAME_BUILTIN_COMMAND_MODULE = '@appium/base-driver'; * Converts declarations to information about Appium commands */ export class CommandConverter { + /** + * The project context of TypeDoc + */ #ctx: Context; + + /** + * Custom logger + */ #log: AppiumPluginLogger; /** @@ -108,7 +132,7 @@ export class CommandConverter { for (const mod of modules) { this.#log.verbose('Converting module %s', mod.name); const cmdInfo = this.#convertModuleClasses(mod); - if (cmdInfo.hasCommands) { + if (cmdInfo.hasData) { projectCommands.set(mod, this.#convertModuleClasses(mod)); } this.#log.info('Converted module %s', mod.name); @@ -162,13 +186,13 @@ export class CommandConverter { * @param refl A class which may contain an `executeMethodMap` static property * @returns List of "execute commands", if any */ - #convertExecuteMethodMap(refl: DeclarationReflectionWithReflectedType): ExecCommandDataSet { + #convertExecuteMethodMap(refl: DeclarationReflectionWithReflectedType): ExecMethodDataSet { const executeMethodMap = findChildByNameAndGuard( refl, NAME_EXECUTE_METHOD_MAP, isExecMethodDefReflection ); - const commandRefs: ExecCommandDataSet = new Set(); + const commandRefs: ExecMethodDataSet = new Set(); if (!executeMethodMap) { // no execute commands in this class return commandRefs; @@ -315,7 +339,7 @@ export class CommandConverter { */ #convertModuleClasses(parent: ParentReflection) { let routes: RouteMap = new Map(); - let executeCommands: ExecCommandDataSet = new Set(); + let executeMethods: ExecMethodDataSet = new Set(); const classReflections = parent .getChildrenByKind(ReflectionKind.Class) @@ -333,12 +357,12 @@ export class CommandConverter { const executeMethodMap = this.#convertExecuteMethodMap(classRefl); if (executeMethodMap.size) { - executeCommands = new Set([...executeCommands, ...executeMethodMap]); + executeMethods = new Set([...executeMethods, ...executeMethodMap]); } this.#log.verbose('Converted class %s', classRefl.name); } - return new CommandInfo(routes, executeCommands); + return new CommandInfo(routes, executeMethods); } /** diff --git a/packages/typedoc-plugin-appium/lib/converter/types.ts b/packages/typedoc-plugin-appium/lib/converter/types.ts index e2f5e5ac0..936439305 100644 --- a/packages/typedoc-plugin-appium/lib/converter/types.ts +++ b/packages/typedoc-plugin-appium/lib/converter/types.ts @@ -1,3 +1,4 @@ +import {Merge} from 'type-fest'; import { DeclarationReflection, LiteralType, @@ -10,40 +11,72 @@ import { import {AllowedHttpMethod} from '../model'; import {NAME_BUILTIN_COMMAND_MODULE, NAME_METHOD_MAP, NAME_NEW_METHOD_MAP} from './converter'; -export type MethodMapDeclarationReflection = DeclarationReflectionWithReflectedType & { - name: typeof NAME_METHOD_MAP | typeof NAME_NEW_METHOD_MAP; -}; +/** + * Type corresponding to a reflection of a {@linkcode @appium/types!MethodMap} + */ +export type MethodMapDeclarationReflection = Merge< + DeclarationReflectionWithReflectedType, + {name: typeof NAME_METHOD_MAP | typeof NAME_NEW_METHOD_MAP} +>; -export type BaseDriverDeclarationReflection = DeclarationReflection & { - name: typeof NAME_BUILTIN_COMMAND_MODULE; -}; +/** + * Type corresponding to a reflection of {@linkcode @appium/base-driver!} + */ +export type BaseDriverDeclarationReflection = Merge< + DeclarationReflection, + { + name: typeof NAME_BUILTIN_COMMAND_MODULE; + kind: ReflectionKind.Module; + } +>; -export type WithType = {type: T}; +/** + * Utility to narrow a declaration reflection to a specific `SomeType` + */ +type WithSomeType< + T extends SomeType, + R extends DeclarationReflection = DeclarationReflection +> = Merge; -export type MethodDefParamsDeclarationReflection = DeclarationReflection & - WithType< - WithReadonlyOperator & { - target: TupleType & { - elements: LiteralType[]; - }; - } - >; +/** + * Utility; a TupleType with literal elements + */ +type TupleTypeWithLiteralElements = Merge; -export type WithReadonlyOperator = TypeOperatorType & {operator: 'readonly'}; +/** + * Type for the parameters of a command definition or execute method definition. + * + * Node that merging `TypeOperatorType` won't work because it will no longer satisfy `SomeType`, because `SomeType` is a finite collection. + */ +export type MethodDefParamsDeclarationReflection = WithSomeType< + TypeOperatorType & {operator: 'readonly'; target: TupleTypeWithLiteralElements} +>; -export type RoutePropDeclarationReflection = DeclarationReflectionWithReflectedType & { - kind: ReflectionKind.Property; -}; +/** + * Narrows a declaration reflection to one having a reflection type and a property kind. Generic + */ +export type PropDeclarationReflection = Merge< + DeclarationReflectionWithReflectedType, + {kind: ReflectionKind.Property} +>; -export type HTTPMethodDeclarationReflection = DeclarationReflectionWithReflectedType & { - kind: ReflectionKind.Property; - originalName: AllowedHttpMethod; -}; +/** + * A type corresponding to the HTTP method of a route, which is a property off of the object with the route name in a `MethodMap` + */ +export type HTTPMethodDeclarationReflection = Merge< + PropDeclarationReflection, + {originalName: AllowedHttpMethod} +>; -export type DeclarationReflectionWithReflectedType = DeclarationReflection & - WithType; +/** + * A declaration reflection having a reflection type. Generic + */ +export type DeclarationReflectionWithReflectedType = WithSomeType; -export type CommandPropDeclarationReflection = DeclarationReflection & WithType; +/** + * Type corresponding to the value of the `command` property within a `MethodDef`, which must be a type literal. + */ +export type CommandPropDeclarationReflection = WithSomeType; /** * A generic type guard diff --git a/packages/typedoc-plugin-appium/lib/guards.ts b/packages/typedoc-plugin-appium/lib/guards.ts index 8e0636dd0..72b0e6ccc 100644 --- a/packages/typedoc-plugin-appium/lib/guards.ts +++ b/packages/typedoc-plugin-appium/lib/guards.ts @@ -1,10 +1,14 @@ +/** + * A bunch of type guards. Because here is a place to put all of them. + * @module + */ + import { DeclarationReflection, ReflectionType, TypeOperatorType, TupleType, LiteralType, - IntrinsicType, ReflectionKind, Reflection, } from 'typedoc'; @@ -21,32 +25,70 @@ import { HTTPMethodDeclarationReflection, MethodDefParamsDeclarationReflection, MethodMapDeclarationReflection, - RoutePropDeclarationReflection, + PropDeclarationReflection, } from './converter/types'; -import {AllowedHttpMethod} from './model'; +import {AllowedHttpMethod, ExecMethodData} from './model'; +/** + * Set of HTTP methods allowed by WebDriver; see {@linkcode AllowedHttpMethod} + */ +const ALLOWED_HTTP_METHODS: Readonly> = new Set([ + 'GET', + 'POST', + 'DELETE', +] as const); + +/** + * Type guard for {@linkcode DeclarationReflection} + * @param value any value + */ export function isDeclarationReflection(value: any): value is DeclarationReflection { return value instanceof DeclarationReflection; } + +/** + * Type guard for {@linkcode ReflectionType} + * @param value any value + */ export function isReflectionType(value: any): value is ReflectionType { return value instanceof ReflectionType; } + +/** + * Type guard for {@linkcode TypeOperatorType} + * @param value any value + */ export function isTypeOperatorType(value: any): value is TypeOperatorType { return value instanceof TypeOperatorType; } + +/** + * Type guard for {@linkcode LiteralType} + * @param value any value + */ export function isLiteralType(value: any): value is LiteralType { return value instanceof LiteralType; } -export function isIntrinsicType(value: any): value is IntrinsicType { - return value instanceof IntrinsicType; -} + +/** + * Type guard for {@linkcode TupleType} + * @param value any value + */ export function isTupleType(value: any): value is TupleType { return value instanceof TupleType; } +/** + * Type guard for a {@linkcode DeclarationReflectionWithReflectedType} corresponding to + * the `executeMethodMap` static property of an extension class. + * @param value any + */ export function isExecMethodDefReflection( value: any -): value is DeclarationReflectionWithReflectedType { +): value is DeclarationReflectionWithReflectedType & { + name: typeof NAME_EXECUTE_METHOD_MAP; + flags: {isStatic: true}; +} { return ( isReflectionWithReflectedType(value) && value.name === NAME_EXECUTE_METHOD_MAP && @@ -54,6 +96,10 @@ export function isExecMethodDefReflection( ); } +/** + * Type guard for a {@linkcode MethodDefParamsDeclarationReflection} corresponding to a list of required or optional parameters within a command or execute method definition. + * @param value any value + */ export function isParamsArray(value: any): value is MethodDefParamsDeclarationReflection { return ( isDeclarationReflection(value) && @@ -63,12 +109,18 @@ export function isParamsArray(value: any): value is MethodDefParamsDeclarationRe ); } -export function isRoutePropDeclarationReflection( - value: any -): value is RoutePropDeclarationReflection { +/** + * Type guard for a {@linkcode PropDeclarationReflection} corresponding to some property of a constant object. + * @param value any value + */ +export function isRoutePropDeclarationReflection(value: any): value is PropDeclarationReflection { return isReflectionWithReflectedType(value) && isPropertyKind(value); } +/** + * Type guard for a {@linkcode BaseDriverDeclarationReflection} corresponding to the `@appium/base-driver` module (_not_ the class). + * @param value any value + */ export function isBaseDriverDeclarationReflection( value: any ): value is BaseDriverDeclarationReflection { @@ -79,10 +131,21 @@ export function isBaseDriverDeclarationReflection( ); } +/** + * Type guard for a property of an object (a {@linkcode Reflection} having kind {@linkcode ReflectionKind.Property}). + * @param value any value + * @returns + */ export function isPropertyKind(value: any) { return value instanceof Reflection && value.kindOf(ReflectionKind.Property); } +/** + * Type guard for a {@linkcode MethodMapDeclarationReflection} corresponding to the `newMethodMap` static property of an extension class _or_ the `METHOD_MAP` export within `@appium/base-driver`. + * + * Note that the type does not care about the `isStatic` flag, but this guard does. + * @param value any value + */ export function isMethodMapDeclarationReflection( value: any ): value is MethodMapDeclarationReflection { @@ -92,13 +155,18 @@ export function isMethodMapDeclarationReflection( ); } +/** + * Type guard for a {@linkcode DeclarationReflectionWithReflectedType} a declaration reflection having a reflection type. + * + * I don't know what that means, exactly, but there it is. + * @param value any value + */ export function isReflectionWithReflectedType( value: any ): value is DeclarationReflectionWithReflectedType { return isDeclarationReflection(value) && isReflectionType(value.type); } -const ALLOWED_HTTP_METHODS = Object.freeze(new Set(['GET', 'POST', 'DELETE'])); export function isHTTPMethodDeclarationReflection( value: any ): value is HTTPMethodDeclarationReflection { @@ -109,12 +177,29 @@ export function isHTTPMethodDeclarationReflection( ); } +/** + * Type guard for an {@linkcode AllowedHttpMethod} + * + * @param value any value + */ export function isAllowedHTTPMethod(value: any): value is AllowedHttpMethod { return ALLOWED_HTTP_METHODS.has(value); } +/** + * Type guard for a {@linkcode CommandPropDeclarationReflection} corresponding to the `command` property of a {@linkcode @appium/types!MethodDef} object contained within a {@linkcode @appium/types!MethodMap}. + * @param value any value + */ export function isCommandPropDeclarationReflection( value: any ): value is CommandPropDeclarationReflection { return isDeclarationReflection(value) && isLiteralType(value.type); } + +/** + * Type guard for a {@linkcode ExecMethodData} derived from a {@linkcode @appium/types!ExecuteMethodMap} object. + * @param value any value + */ +export function isExecMethodData(value: any): value is ExecMethodData { + return value && typeof value === 'object' && value.script; +} diff --git a/packages/typedoc-plugin-appium/lib/logger.ts b/packages/typedoc-plugin-appium/lib/logger.ts index 2ee84a172..eb3094f77 100644 --- a/packages/typedoc-plugin-appium/lib/logger.ts +++ b/packages/typedoc-plugin-appium/lib/logger.ts @@ -12,12 +12,17 @@ import {format} from 'node:util'; import {Logger, LogLevel} from 'typedoc'; -const LogMethods = { - [LogLevel.Error]: 'error', - [LogLevel.Warn]: 'warn', - [LogLevel.Info]: 'info', - [LogLevel.Verbose]: 'verbose', -} as const; +/** + * Mapping of TypeDoc {@linkcode LogLevel}s to method names. + */ +const LogMethods: Readonly< + Map> +> = new Map([ + [LogLevel.Error, 'error'], + [LogLevel.Warn, 'warn'], + [LogLevel.Info, 'info'], + [LogLevel.Verbose, 'verbose'], +]); export class AppiumPluginLogger extends Logger { /** @@ -142,10 +147,13 @@ export class AppiumPluginLogger extends Logger { if (this.#logThroughParent) { this.#logThroughParent(level, ns, message, ...args); } else { - const parentMethod = LogMethods[level]; + const parentMethod = LogMethods.get(level)!; this.#parent[parentMethod](this.#formatMessage(ns, message, ...args)); } } } -type ParentLogger = (level: LogLevel, message: string, ...args: any[]) => void; +/** + * Used internally by {@link AppiumPluginLogger.createChildLogger} to pass log messages to the parent. + */ +export type ParentLogger = (level: LogLevel, message: string, ...args: any[]) => void; diff --git a/packages/typedoc-plugin-appium/lib/model/command-info.ts b/packages/typedoc-plugin-appium/lib/model/command-info.ts index 4c1a027a2..6f5cb1a16 100644 --- a/packages/typedoc-plugin-appium/lib/model/command-info.ts +++ b/packages/typedoc-plugin-appium/lib/model/command-info.ts @@ -1,18 +1,19 @@ -import {ExecCommandDataSet, RouteMap} from './types'; +import {ExecMethodDataSet, RouteMap} from './types'; /** - * Data structure describing routes and commands for a particular module (or project) + * Data structure describing routes and commands for a particular module (or project), + * including execute methods (if any) */ export class CommandInfo { constructor( public readonly routeMap: RouteMap, - public readonly execCommandDataSet: ExecCommandDataSet = new Set() + public readonly execMethodDataSet: ExecMethodDataSet = new Set() ) {} /** - * `true` if this instance has some actual data + * Returns `true` if this instance has some actual data */ - public get hasCommands() { - return Boolean(this.execCommandDataSet.size + this.routeMap.size); + public get hasData() { + return Boolean(this.execMethodDataSet.size + this.routeMap.size); } } diff --git a/packages/typedoc-plugin-appium/lib/model/reflection/command.ts b/packages/typedoc-plugin-appium/lib/model/reflection/command.ts index 8df3a2166..dac66cefd 100644 --- a/packages/typedoc-plugin-appium/lib/model/reflection/command.ts +++ b/packages/typedoc-plugin-appium/lib/model/reflection/command.ts @@ -1,67 +1,117 @@ -import {Comment} from 'typedoc'; -import {CommandData, ExecCommandData, ParentReflection, Route} from '../types'; +import {Comment, DeclarationReflection} from 'typedoc'; +import {isExecMethodData} from '../../guards'; +import {AllowedHttpMethod, CommandData, ExecMethodData, Route} from '../types'; import {CommandsReflection} from './commands'; import {AppiumPluginReflectionKind} from './kind'; -import {AppiumPluginReflection} from './plugin'; /** - * The route will be this + * Execute Methods all have the same route. */ export const NAME_EXECUTE_ROUTE = '/session/:sessionId/execute'; +/** + * Execute methods all have the same HTTP method. + */ export const HTTP_METHOD_EXECUTE = 'POST'; -export class CommandReflection extends AppiumPluginReflection { +/** + * A reflection containing data about a single command or execute method. + * + * Methods may be invoked directly by Handlebars templates. + */ +export class CommandReflection extends DeclarationReflection { + /** + * HTTP Method of the command or execute method + */ public readonly httpMethod: string; + + /** + * Optional parameters, if any + */ public readonly optionalParams: string[]; + + /** + * Required parameters, if any + */ public readonly requiredParams: string[]; + + /** + * Route name + */ public readonly route: Route; + + /** + * Script name, if any. Only used if kind is `EXECUTE_METHOD` + */ public readonly script?: string; + + /** + * Comment, if any. + */ public readonly comment?: Comment; + /** + * Sets props depending on type of `data` + * @param data Command or execute method data + * @param parent Always a {@linkcode CommandsReflection} + * @param route Route, if not an execute method + */ constructor( - readonly commandRef: CommandData | ExecCommandData, + readonly data: CommandData | ExecMethodData, parent: CommandsReflection, - route: Route = NAME_EXECUTE_ROUTE + route?: Route ) { let name: string; let kind: AppiumPluginReflectionKind; + let script: string | undefined; + let httpMethod: AllowedHttpMethod; - if (CommandReflection.isExecCommandData(commandRef)) { - name = commandRef.script; - kind = AppiumPluginReflectionKind.EXECUTE_COMMAND; + // common data + const {requiredParams, optionalParams, comment} = data; + + // kind-specific data + if (isExecMethodData(data)) { + script = name = data.script; + kind = AppiumPluginReflectionKind.EXECUTE_METHOD; + route = NAME_EXECUTE_ROUTE; + httpMethod = HTTP_METHOD_EXECUTE; } else { + if (!route) { + throw new TypeError('"route" arg is required for a non-execute-method command'); + } name = route; kind = AppiumPluginReflectionKind.COMMAND; + httpMethod = data.httpMethod; } + super(name, kind as any, parent); this.route = route; - this.httpMethod = 'httpMethod' in commandRef ? commandRef.httpMethod : HTTP_METHOD_EXECUTE; - this.requiredParams = commandRef.requiredParams ?? []; - this.optionalParams = commandRef.optionalParams ?? []; - this.script = CommandReflection.isExecCommandData(commandRef) ? commandRef.script : undefined; - this.comment = commandRef.comment; + this.httpMethod = httpMethod; + this.requiredParams = requiredParams ?? []; + this.optionalParams = optionalParams ?? []; + this.script = script; + this.comment = comment; } + /** + * If `true`, this command has required parameters + */ public get hasRequiredParams(): boolean { return Boolean(this.requiredParams.length); } + /** + * If `true`, this command has optional parameters + */ public get hasOptionalParams(): boolean { return Boolean(this.optionalParams.length); } - public get isExecuteCommand(): boolean { - return Boolean(this.script && this.route === NAME_EXECUTE_ROUTE); - } - /** - * Type guard for execute command refs - * @param ref Command reference - * @returns `true` if it's an execute command + * If `true`, this command contains data about an execute method */ - public static isExecCommandData(ref: CommandData | ExecCommandData): ref is ExecCommandData { - return 'script' in ref; + public get isExecuteMethod(): boolean { + return this.kindOf(AppiumPluginReflectionKind.EXECUTE_METHOD as any); } } diff --git a/packages/typedoc-plugin-appium/lib/model/reflection/commands.ts b/packages/typedoc-plugin-appium/lib/model/reflection/commands.ts index 4dd90786e..f0f75872d 100644 --- a/packages/typedoc-plugin-appium/lib/model/reflection/commands.ts +++ b/packages/typedoc-plugin-appium/lib/model/reflection/commands.ts @@ -1,24 +1,28 @@ +import {DeclarationReflection} from 'typedoc'; import {CommandInfo} from '../command-info'; -import {ExecCommandDataSet, ParentReflection, RouteMap} from '../types'; - +import {ExecMethodDataSet, ParentReflection, RouteMap} from '../types'; import {AppiumPluginReflectionKind} from './kind'; -import {AppiumPluginReflection} from './plugin'; /** - * A Reflection representing a set of commands within a module or project + * A reflection containing data about commands and/or execute methods. + * + * Methods may be invoked directly by Handlebars templates. */ -export class CommandsReflection extends AppiumPluginReflection { +export class CommandsReflection extends DeclarationReflection { /** - * A set of objects + * Info about execute methods + */ + public readonly execMethodDataSet: ExecMethodDataSet; + /** + * Info about routes/commands */ - public readonly execCommandDataSet: ExecCommandDataSet; public readonly routeMap: RouteMap; - constructor(name: string, parent: ParentReflection, commands: CommandInfo) { + constructor(name: string, parent: ParentReflection, {routeMap, execMethodDataSet}: CommandInfo) { super(name, AppiumPluginReflectionKind.COMMANDS as any, parent); this.parent = parent; - this.routeMap = commands.routeMap; - this.execCommandDataSet = commands.execCommandDataSet; + this.routeMap = routeMap; + this.execMethodDataSet = execMethodDataSet; } /** @@ -26,8 +30,8 @@ export class CommandsReflection extends AppiumPluginReflection { * * Used by templates */ - public get hasExecuteCommands(): boolean { - return Boolean(this.execCommandDataSet.size); + public get hasExecuteMethod(): boolean { + return Boolean(this.execMethodCount); } /** @@ -35,7 +39,21 @@ export class CommandsReflection extends AppiumPluginReflection { * * Used by templates */ - public get hasRoutes(): boolean { - return Boolean(this.routeMap.size); + public get hasRoute(): boolean { + return Boolean(this.routeCount); + } + + /** + * Returns number of routes ("commands") in this in this data + */ + public get routeCount(): number { + return this.routeMap.size; + } + + /** + * Returns number of execute methods in this data + */ + public get execMethodCount(): number { + return this.execMethodDataSet.size; } } diff --git a/packages/typedoc-plugin-appium/lib/model/reflection/index.ts b/packages/typedoc-plugin-appium/lib/model/reflection/index.ts index a9b80aa51..2f722e35f 100644 --- a/packages/typedoc-plugin-appium/lib/model/reflection/index.ts +++ b/packages/typedoc-plugin-appium/lib/model/reflection/index.ts @@ -1,4 +1,3 @@ export * from './command'; export * from './commands'; export * from './kind'; -export * from './plugin'; diff --git a/packages/typedoc-plugin-appium/lib/model/reflection/kind.ts b/packages/typedoc-plugin-appium/lib/model/reflection/kind.ts index 3a29d1fba..ce517db87 100644 --- a/packages/typedoc-plugin-appium/lib/model/reflection/kind.ts +++ b/packages/typedoc-plugin-appium/lib/model/reflection/kind.ts @@ -1,7 +1,17 @@ +/** + * Declares new "kinds" within TypeDoc. + * + * A "kind" is a way for TypeDoc to understand how to document something. Mostly, these have a 1:1 relationship with some sort of TypeScript concept. This is unsuitable for our purposes, since there's no notion of a "command" or "execute method" in TypeScript. To that end, we must create new ones. + * + * Note that _creating new `ReflectionKind`s is a hack_ and is not supported by TypeDoc. This is the reason you will see `as any` wherever a {@linkcode AppiumPluginReflectionKind} is used. + */ + import {addReflectionKind} from './utils'; /** - * Namespace for our reflection kinds + * Namespace for our reflection kinds. + * + * The only reason we use a namespace is to avoid conflicts with TypeDoc or other plugins monkeying around in the "kind" system. */ export const NS = 'appium'; @@ -11,6 +21,6 @@ export const NS = 'appium'; export enum AppiumPluginReflectionKind { COMMANDS = addReflectionKind(NS, 'Commands'), COMMAND = addReflectionKind(NS, 'Command'), - EXECUTE_COMMAND = addReflectionKind(NS, 'ExecuteCommand'), - ANY = addReflectionKind(NS, 'Any', COMMAND | EXECUTE_COMMAND | COMMANDS), + EXECUTE_METHOD = addReflectionKind(NS, 'ExecuteMethod'), + ANY = addReflectionKind(NS, 'Any', COMMAND | EXECUTE_METHOD | COMMANDS), } diff --git a/packages/typedoc-plugin-appium/lib/model/reflection/plugin.ts b/packages/typedoc-plugin-appium/lib/model/reflection/plugin.ts deleted file mode 100644 index 3160fec90..000000000 --- a/packages/typedoc-plugin-appium/lib/model/reflection/plugin.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {DeclarationReflection} from 'typedoc'; - -/** - * This exists just to allow `instanceof` checks - */ -export abstract class AppiumPluginReflection extends DeclarationReflection {} diff --git a/packages/typedoc-plugin-appium/lib/model/reflection/utils.ts b/packages/typedoc-plugin-appium/lib/model/reflection/utils.ts index 9c14fd107..15fb72aba 100644 --- a/packages/typedoc-plugin-appium/lib/model/reflection/utils.ts +++ b/packages/typedoc-plugin-appium/lib/model/reflection/utils.ts @@ -4,6 +4,7 @@ * Copyright (c) 2022 KnodesCommunity * Licensed MIT * @see https://github.com/KnodesCommunity/typedoc-plugins/blob/05717565fae14357b1c4be8122f3156e1d46d332/LICENSE + * @module */ import {ReflectionKind} from 'typedoc'; diff --git a/packages/typedoc-plugin-appium/lib/model/types.ts b/packages/typedoc-plugin-appium/lib/model/types.ts index 415bbd3ff..da0219f7e 100644 --- a/packages/typedoc-plugin-appium/lib/model/types.ts +++ b/packages/typedoc-plugin-appium/lib/model/types.ts @@ -32,7 +32,7 @@ export type CommandMap = Map; export type ModuleCommands = Map; /** - * Common fields for a {@linkcode CommandData} or {@linkcode ExecCommandData} + * Common fields for a {@linkcode CommandData} or {@linkcode ExecMethodData} */ export interface BaseCommandData { /** @@ -76,7 +76,7 @@ export interface CommandData extends BaseCommandData { * * All of these share the same `execute` route, so it is omitted from this interface. */ -export interface ExecCommandData extends BaseCommandData { +export interface ExecMethodData extends BaseCommandData { script: string; } @@ -91,6 +91,6 @@ export type ParentReflection = DeclarationReflection | ProjectReflection; export type RouteMap = Map; /** - * A set of {@linkcode ExecCommandData} objects + * A set of {@linkcode ExecMethodData} objects */ -export type ExecCommandDataSet = Set; +export type ExecMethodDataSet = Set; diff --git a/packages/typedoc-plugin-appium/lib/output/index.ts b/packages/typedoc-plugin-appium/lib/output/index.ts deleted file mode 100644 index 7b1f54ecf..000000000 --- a/packages/typedoc-plugin-appium/lib/output/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './theme'; diff --git a/packages/typedoc-plugin-appium/lib/output/theme/appium.ts b/packages/typedoc-plugin-appium/lib/output/theme/appium.ts deleted file mode 100644 index d9248d4ef..000000000 --- a/packages/typedoc-plugin-appium/lib/output/theme/appium.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {ContainerReflection, PageEvent, Renderer} from 'typedoc'; -import {MarkdownTheme} from 'typedoc-plugin-markdown'; -import {AppiumPluginLogger} from '../../logger'; -import {AppiumPluginReflectionKind} from '../../model'; -import {compileTemplate, registerHelpers, Template} from './utils'; - -/** - * Name of the theme; used at definition time - */ -export const THEME_NAME = 'appium'; - -/** - * Factory for `AppiumTheme` class; needs custom logger otherwise inaccessible - * @param log - Custom logger - * @returns `AppiumTheme` class - */ -export function getTheme(log: AppiumPluginLogger): new (renderer: Renderer) => MarkdownTheme { - return class AppiumTheme extends MarkdownTheme { - #log = log.createChildLogger('theme'); - - #commandsTemplateRenderer: TemplateRenderer; - - constructor(renderer: Renderer) { - super(renderer); - - this.#commandsTemplateRenderer = this.#getTemplate(Template.Commands); - - // the intent is to have mkdocs render breadcrumbs - this.hideBreadcrumbs = true; - - // this ensures we can overwrite MarkdownTheme's Handlebars helpers - registerHelpers(); - } - - public override get mappings() { - return [ - { - kind: [AppiumPluginReflectionKind.COMMANDS as any], - isLeaf: true, - directory: 'commands', - template: this.#commandsTemplateRenderer, - }, - ...super.mappings, - ]; - } - - #getTemplate(template: Template): TemplateRenderer { - const render = compileTemplate(template); - return (pageEvent: PageEvent) => { - this.#log.verbose('Rendering template for model %s', pageEvent.model.name); - return render(pageEvent, { - allowProtoMethodsByDefault: true, - allowProtoPropertiesByDefault: true, - data: {theme: this}, - }); - }; - } - }; -} - -type TemplateRenderer = (pageEvent: PageEvent) => string; diff --git a/packages/typedoc-plugin-appium/lib/output/theme/index.ts b/packages/typedoc-plugin-appium/lib/output/theme/index.ts deleted file mode 100644 index 16a733b24..000000000 --- a/packages/typedoc-plugin-appium/lib/output/theme/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './types'; -export * from './appium'; diff --git a/packages/typedoc-plugin-appium/lib/output/theme/types.ts b/packages/typedoc-plugin-appium/lib/output/theme/types.ts deleted file mode 100644 index 8102319d9..000000000 --- a/packages/typedoc-plugin-appium/lib/output/theme/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {RenderTemplate, RendererEvent, Theme} from 'typedoc'; -import {CommandReflection} from '../../model'; - -export type RenderCommandLinkProps = {page: CommandReflection; label?: string}; -export interface IAppiumPluginThemeMethods { - renderPageLink: RenderTemplate; -} -export interface IAppiumPluginTheme extends Theme { - appiumPlugin(event: RendererEvent): IAppiumPluginThemeMethods; -} - -export function isAppiumPluginTheme(theme: Theme): theme is IAppiumPluginTheme { - return 'appiumPlugin' in theme; -} diff --git a/packages/typedoc-plugin-appium/lib/output/theme/utils.ts b/packages/typedoc-plugin-appium/lib/output/theme/utils.ts deleted file mode 100644 index 0cc3d2735..000000000 --- a/packages/typedoc-plugin-appium/lib/output/theme/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -import _ from 'lodash'; -import fs from 'node:fs'; -import path from 'node:path'; -import Handlebars from 'handlebars'; -import {ContainerReflection, PageEvent, ReflectionKind} from 'typedoc'; -import {AppiumPluginReflectionKind} from '../../model'; - -const RESOURCES_PATH = path.join(__dirname, '..', '..', '..', 'resources'); -const TEMPLATE_PATH = path.join(RESOURCES_PATH, 'templates'); -const PARTIALS_PATH = path.join(RESOURCES_PATH, 'partials'); - -const Partials = { - command: 'command.hbs', - executeCommand: 'execute-command.hbs', -} as const; - -function registerPartials() { - for (const [name, filename] of Object.entries(Partials)) { - Handlebars.registerPartial(name, fs.readFileSync(path.join(PARTIALS_PATH, filename), 'utf8')); - } -} - -registerPartials(); - -export enum Template { - Commands = 'commands.hbs', -} - -export const compileTemplate = _.memoize((template: Template) => { - const templatePath = path.join(TEMPLATE_PATH, template); - return Handlebars.compile(fs.readFileSync(templatePath, 'utf8')); -}); - -export function registerHelpers() { - Handlebars.registerHelper('reflectionPath', function (this: PageEvent) { - if (this.model) { - if (this.model.kind && this.model.kind !== ReflectionKind.Module) { - if (this.model.kind === (AppiumPluginReflectionKind.COMMANDS as any)) { - return `${this.model.name} Commands`; - } - 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; - }); -} diff --git a/packages/typedoc-plugin-appium/lib/plugin.ts b/packages/typedoc-plugin-appium/lib/plugin.ts index 4537e4afb..a460b99b9 100644 --- a/packages/typedoc-plugin-appium/lib/plugin.ts +++ b/packages/typedoc-plugin-appium/lib/plugin.ts @@ -1,7 +1,7 @@ import {Application, Context, Converter} from 'typedoc'; import {convertCommands, createReflections} from './converter'; import {AppiumPluginLogger} from './logger'; -import {getTheme, THEME_NAME} from './output'; +import {AppiumTheme, THEME_NAME} from './theme'; /** * Loads the Appium TypeDoc plugin @@ -11,7 +11,7 @@ export function load(app: Application) { const log = new AppiumPluginLogger(app.logger, 'appium'); // register our custom theme. the user still has to choose it - app.renderer.defineTheme(THEME_NAME, getTheme(log)); + app.renderer.defineTheme(THEME_NAME, AppiumTheme); app.converter.on(Converter.EVENT_RESOLVE_BEGIN, (ctx: Context) => { // we don't want to do this work if we're not using the custom theme! @@ -22,6 +22,10 @@ export function load(app: Application) { // this creates new custom reflections from the data we gathered and registers them // with TypeDoc createReflections(ctx, log, projectCommands); + } else { + log.warn('Not using the Appium theme; skipping command reflection creation'); } }); } + +export * from './theme'; diff --git a/packages/typedoc-plugin-appium/lib/theme/helpers.ts b/packages/typedoc-plugin-appium/lib/theme/helpers.ts new file mode 100644 index 000000000..0dd41374b --- /dev/null +++ b/packages/typedoc-plugin-appium/lib/theme/helpers.ts @@ -0,0 +1,63 @@ +/** + * Custom Handlebars helpers + * @module + */ + +import Handlebars from 'handlebars'; +import {PageEvent, ContainerReflection, ReflectionKind} from 'typedoc'; +import {AppiumPluginReflectionKind} from '../model'; +import plural from 'pluralize'; + +/** + * Overwrites {@linkcode typedoc-plugin-markdown!MarkdownTheme}'s `reflectionPath` helper to handle {@linkcode AppiumPluginReflectionKind} reflection kinds + * @param this Page event + * @returns Reflection path, if any + */ +function reflectionPath(this: PageEvent) { + if (this.model) { + if (this.model.kind && this.model.kind !== ReflectionKind.Module) { + if (this.model.kind === (AppiumPluginReflectionKind.COMMANDS as any)) { + return `${this.model.name} Commands`; + } + 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; +} + +/** + * Helper to "pluralize" a string. + * @param value String to pluralize + * @param count Number of items to consider + * @param inclusive Whether to show the count in the output + * @returns The pluralized string (if necessary) + */ +function pluralize(value: string, count: number, inclusive: boolean = false) { + const safeValue = Handlebars.escapeExpression(value); + // XXX: Handlebars seems to be passing in a truthy value here, even if the arg is unused in the template! Make double-sure it's a boolean. + inclusive = inclusive === true; + const pluralValue = plural(safeValue, count, inclusive); + return new Handlebars.SafeString(pluralValue); +} + +/** + * Registers all custom helpers with Handlebars + */ +export function registerHelpers() { + Handlebars.registerHelper('reflectionPath', reflectionPath); + Handlebars.registerHelper('pluralize', pluralize); +} diff --git a/packages/typedoc-plugin-appium/lib/theme/index.ts b/packages/typedoc-plugin-appium/lib/theme/index.ts new file mode 100644 index 000000000..d53d80bd6 --- /dev/null +++ b/packages/typedoc-plugin-appium/lib/theme/index.ts @@ -0,0 +1,98 @@ +import {ContainerReflection, PageEvent, ReflectionKind, Renderer} from 'typedoc'; +import {MarkdownTheme} from 'typedoc-plugin-markdown'; +import {AppiumPluginLogger} from '../logger'; +import {AppiumPluginReflectionKind, NS} from '../model'; +import {registerHelpers} from './helpers'; +import {compileTemplate, AppiumThemeTemplate} from './template'; + +/** + * Name of the theme; used at definition time + */ +export const THEME_NAME = 'appium'; + +export class AppiumTheme extends MarkdownTheme { + /** + * A template renderer for `CommandReflection`s + */ + #commandsTemplateRenderer: TemplateRenderer; + + /** + * Custom logger. This is not the same as the one created by the plugin loader. + */ + #log: AppiumPluginLogger; + + /** + * Creates template renderers and registers all {@linkcode Handlebars} helpers. + * @param renderer - TypeDoc renderer + * + * @todo Make `hideBreadcrumbs` configurable + */ + constructor(renderer: Renderer) { + super(renderer); + + // ideally, this would be a child of the logger created by the `load()` function, + // but I don't know how to get at it. + this.#log = new AppiumPluginLogger(renderer.owner.logger, `${NS}:theme`); + + this.#commandsTemplateRenderer = this.#createTemplateRenderer(AppiumThemeTemplate.Commands); + + // the intent is to have mkdocs render breadcrumbs + this.hideBreadcrumbs = true; + + // this ensures we can overwrite MarkdownTheme's Handlebars helpers + registerHelpers(); + } + + /** + * This is essentially a lookup of {@linkcode ReflectionKind}s to templates. It also controls in which directory the output files live. + * + * If `isLeaf` is `false`, the model gets its own document. + */ + public override get mappings(): TemplateMapping[] { + return [ + { + kind: [AppiumPluginReflectionKind.COMMANDS as any], + isLeaf: false, + directory: 'commands', + template: this.#commandsTemplateRenderer, + }, + ...super.mappings, + ]; + } + + /** + * Given a {@linkcode AppiumThemeTemplate} return a function which will render the template + * given some data. + * @param template Template to render + * @returns Rendering function + */ + #createTemplateRenderer(template: AppiumThemeTemplate): TemplateRenderer { + const render = compileTemplate(template); + return (pageEvent: PageEvent) => { + this.#log.verbose('Rendering template for model %s', pageEvent.model.name); + return render(pageEvent, { + allowProtoMethodsByDefault: true, + allowProtoPropertiesByDefault: true, + data: {theme: this}, + }); + }; + } +} + +/** + * A function which accepts {@linkcode PageEvent} as its model and returns the final markdown. + */ +export type TemplateRenderer = (pageEvent: PageEvent) => string; + +/** + * A mapping of {@linkcode ReflectionKind} to a template and other metadata. + * + * Defined by {@linkcode MarkdownTheme}. + * @public + */ +export type TemplateMapping = { + kind: ReflectionKind[]; + isLeaf: boolean; + directory: string; + template: (pageEvent: PageEvent) => string; +}; diff --git a/packages/typedoc-plugin-appium/lib/theme/template.ts b/packages/typedoc-plugin-appium/lib/theme/template.ts new file mode 100644 index 000000000..855722d45 --- /dev/null +++ b/packages/typedoc-plugin-appium/lib/theme/template.ts @@ -0,0 +1,64 @@ +/** + * Handlebars template & partial helpers + * @module + */ + +import Handlebars from 'handlebars'; +import _ from 'lodash'; +import fs from 'node:fs'; +import path from 'node:path'; + +/** + * Path to resources directory, containing all templates and partials. + */ +const RESOURCES_PATH = path.join(__dirname, '..', '..', 'resources'); + +/** + * Path to templates directory within {@linkcode RESOURCES_PATH} + */ +const TEMPLATE_PATH = path.join(RESOURCES_PATH, 'templates'); + +/** + * Path to partials directory within {@linkcode RESOURCES_PATH} + */ +const PARTIALS_PATH = path.join(RESOURCES_PATH, 'partials'); + +/** + * Enum of all available partials + */ +enum AppiumThemePartial { + command = 'command.hbs', + executeMethod = 'execute-command.hbs', +} + +/** + * Enum of all available templates + */ +export enum AppiumThemeTemplate { + /** + * Template to render a list of commands + */ + Commands = 'commands.hbs', +} + +/** + * Registers all partials found in {@linkcode PARTIALS_PATH} with {@linkcode Handlebars}. + * + * This is executed immediately upon loading this module. + */ +function registerPartials() { + for (const [name, filename] of Object.entries(AppiumThemePartial)) { + console.log('registerPartials:', name, filename); + Handlebars.registerPartial(name, fs.readFileSync(path.join(PARTIALS_PATH, filename), 'utf8')); + } +} + +registerPartials(); + +/** + * Compiles a {@linkcode AppiumThemeTemplate}. + */ +export const compileTemplate = _.memoize((template: AppiumThemeTemplate) => { + const templatePath = path.join(TEMPLATE_PATH, template); + return Handlebars.compile(fs.readFileSync(templatePath, 'utf8')); +}); diff --git a/packages/typedoc-plugin-appium/package.json b/packages/typedoc-plugin-appium/package.json index d2721e10d..97d9cfc26 100644 --- a/packages/typedoc-plugin-appium/package.json +++ b/packages/typedoc-plugin-appium/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "handlebars": "4.7.7", + "pluralize": "8.0.0", "type-fest": "3.2.0", "typedoc-plugin-markdown": "3.13.6" } diff --git a/packages/typedoc-plugin-appium/resources/templates/commands.hbs b/packages/typedoc-plugin-appium/resources/templates/commands.hbs index 10a8bd5fb..ee08acb76 100644 --- a/packages/typedoc-plugin-appium/resources/templates/commands.hbs +++ b/packages/typedoc-plugin-appium/resources/templates/commands.hbs @@ -18,20 +18,20 @@ {{#with model}} -{{#if hasRoutes}} -## Routes +{{#if hasRoute}} +## {{pluralize 'Route' routeCount}} {{#each children}} -{{#unless isExecuteCommand}} +{{#unless isExecuteMethod}} {{> command}} {{/unless}} {{/each}} {{/if}} -{{#if hasExecuteCommands}} -## Execute Scripts +{{#if hasExecuteMethod}} +## Execute {{pluralize 'Method' execMethodCount}} {{#each children}} -{{#if isExecuteCommand}} -{{> executeCommand}} +{{#if isExecuteMethod}} +{{> executeMethod}} {{/if}} {{/each}} {{/if}} diff --git a/typedoc.json b/typedoc.json index c6e26abd1..985b49dc0 100644 --- a/typedoc.json +++ b/typedoc.json @@ -11,6 +11,7 @@ "./packages/fake-driver", "./packages/typedoc-plugin-appium" ], + "excludeInternal": true, "includeVersion": false, "name": "Appium", "out": "typedoc-docs",