diff --git a/CHANGELOG.md b/CHANGELOG.md index a96597bc5..9fd72005a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +### Features + +- Add support for defining the kind sort order, #2109. + ### Bug Fixes - Normalize all file paths on Windows, #2113. diff --git a/src/lib/converter/plugins/GroupPlugin.ts b/src/lib/converter/plugins/GroupPlugin.ts index 33d54c7d3..3fbefadad 100644 --- a/src/lib/converter/plugins/GroupPlugin.ts +++ b/src/lib/converter/plugins/GroupPlugin.ts @@ -8,7 +8,7 @@ import { ReflectionGroup } from "../../models/ReflectionGroup"; import { Component, ConverterComponent } from "../components"; import { Converter } from "../converter"; import type { Context } from "../context"; -import { sortReflections, SortStrategy } from "../../utils/sort"; +import { getSortFunction } from "../../utils/sort"; import { BindOption, removeIf } from "../../utils"; import { Comment } from "../../models"; @@ -38,9 +38,7 @@ export class GroupPlugin extends ConverterComponent { [ReflectionKind.TypeAlias]: "Type Aliases", }; - /** @internal */ - @BindOption("sort") - sortStrategies!: SortStrategy[]; + sortFunction!: (reflections: DeclarationReflection[]) => void; @BindOption("searchGroupBoosts") boosts!: Record; @@ -52,6 +50,9 @@ export class GroupPlugin extends ConverterComponent { */ override initialize() { this.listenTo(this.owner, { + [Converter.EVENT_RESOLVE_BEGIN]: () => { + this.sortFunction = getSortFunction(this.application.options); + }, [Converter.EVENT_RESOLVE]: this.onResolve, [Converter.EVENT_RESOLVE_END]: this.onEndResolve, }); @@ -104,7 +105,7 @@ export class GroupPlugin extends ConverterComponent { reflection.children.length > 0 && !reflection.groups ) { - sortReflections(reflection.children, this.sortStrategies); + this.sortFunction(reflection.children); reflection.groups = this.getReflectionGroups(reflection.children); } } diff --git a/src/lib/utils/enum.ts b/src/lib/utils/enum.ts index f11462380..07edb6abe 100644 --- a/src/lib/utils/enum.ts +++ b/src/lib/utils/enum.ts @@ -21,3 +21,10 @@ export function hasAllFlags(flags: number, check: number): boolean { export function hasAnyFlag(flags: number, check: number): boolean { return (flags & check) !== 0; } + +// Note: String enums are not handled. +export function getEnumKeys(Enum: Record): string[] { + return Object.keys(Enum).filter((k) => { + return Enum[Enum[k]] === k; + }); +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index d0d715aaa..2e4640d99 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -53,7 +53,7 @@ export type { ManuallyValidatedOption, } from "./options"; export { discoverPlugins, loadPlugins } from "./plugins"; -export { sortReflections } from "./sort"; +export { getSortFunction } from "./sort"; export type { SortStrategy } from "./sort"; export { EventHooks } from "./hooks"; diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index e0965ca57..3819d04e0 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -133,6 +133,7 @@ export interface TypeDocOptionMap { defaultCategory: string; categoryOrder: string[]; sort: SortStrategy[]; + kindSortOrder: Array; visibilityFilters: ManuallyValidatedOption<{ protected?: boolean; private?: boolean; diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 0661cd08e..dcc7d50da 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -12,6 +12,7 @@ import { EntryPointStrategy } from "../../entry-point"; import { ReflectionKind } from "../../../models/reflections/kind"; import * as Validation from "../../validation"; import { blockTags, inlineTags, modifierTags } from "../tsdoc-defaults"; +import { getEnumKeys } from "../../enum"; // For convenience, added in the same order as they are documented on the website. export function addTypeDocOptions(options: Pick) { @@ -451,6 +452,28 @@ export function addTypeDocOptions(options: Pick) { } }, }); + options.addDeclaration({ + name: "kindSortOrder", + help: "Specify the sort order for reflections when 'kind' is specified.", + type: ParameterType.Array, + defaultValue: [], + validate(value) { + const invalid = new Set(value); + const valid = getEnumKeys(ReflectionKind); + for (const v of valid) { + invalid.delete(v); + } + + if (invalid.size !== 0) { + throw new Error( + `kindSortOrder may only specify known values, and invalid values were provided (${Array.from( + invalid + ).join(", ")}). The valid kinds are:\n${valid.join(", ")}` + ); + } + }, + }); + options.addDeclaration({ name: "visibilityFilters", help: "Specify the default visibility for builtin filters and additional filters according to modifier tags.", @@ -595,9 +618,7 @@ export function addTypeDocOptions(options: Pick) { type: ParameterType.Array, validate(values) { // this is good enough because the values of the ReflectionKind enum are all numbers - const validValues = Object.values(ReflectionKind).filter( - (v) => typeof v === "string" - ); + const validValues = getEnumKeys(ReflectionKind); for (const kind of values) { if (!validValues.includes(kind)) { diff --git a/src/lib/utils/sort.ts b/src/lib/utils/sort.ts index 650ea6193..f0d76b37e 100644 --- a/src/lib/utils/sort.ts +++ b/src/lib/utils/sort.ts @@ -6,6 +6,7 @@ import { ReflectionKind } from "../models/reflections/kind"; import type { DeclarationReflection } from "../models/reflections/declaration"; import { LiteralType } from "../models/types"; +import type { Options } from "./options"; export const SORT_STRATEGIES = [ "source-order", @@ -21,10 +22,43 @@ export const SORT_STRATEGIES = [ export type SortStrategy = typeof SORT_STRATEGIES[number]; +const defaultKindSortOrder = [ + ReflectionKind.Reference, + ReflectionKind.Project, + ReflectionKind.Module, + ReflectionKind.Namespace, + ReflectionKind.Enum, + ReflectionKind.EnumMember, + ReflectionKind.Class, + ReflectionKind.Interface, + ReflectionKind.TypeAlias, + + ReflectionKind.Constructor, + ReflectionKind.Property, + ReflectionKind.Variable, + ReflectionKind.Function, + ReflectionKind.Accessor, + ReflectionKind.Method, + ReflectionKind.ObjectLiteral, + + ReflectionKind.Parameter, + ReflectionKind.TypeParameter, + ReflectionKind.TypeLiteral, + ReflectionKind.CallSignature, + ReflectionKind.ConstructorSignature, + ReflectionKind.IndexSignature, + ReflectionKind.GetSignature, + ReflectionKind.SetSignature, +] as const; + // Return true if a < b const sorts: Record< SortStrategy, - (a: DeclarationReflection, b: DeclarationReflection) => boolean + ( + a: DeclarationReflection, + b: DeclarationReflection, + data: { kindSortOrder: ReflectionKind[] } + ) => boolean > = { "source-order"(a, b) { const aSymbol = a.project.getSymbolFromReflection(a); @@ -107,53 +141,36 @@ const sorts: Record< "required-first"(a, b) { return !a.flags.isOptional && b.flags.isOptional; }, - kind(a, b) { - const weights = [ - ReflectionKind.Reference, - ReflectionKind.Project, - ReflectionKind.Module, - ReflectionKind.Namespace, - ReflectionKind.Enum, - ReflectionKind.EnumMember, - ReflectionKind.Class, - ReflectionKind.Interface, - ReflectionKind.TypeAlias, - - ReflectionKind.Constructor, - ReflectionKind.Property, - ReflectionKind.Variable, - ReflectionKind.Function, - ReflectionKind.Accessor, - ReflectionKind.Method, - ReflectionKind.ObjectLiteral, - - ReflectionKind.Parameter, - ReflectionKind.TypeParameter, - ReflectionKind.TypeLiteral, - ReflectionKind.CallSignature, - ReflectionKind.ConstructorSignature, - ReflectionKind.IndexSignature, - ReflectionKind.GetSignature, - ReflectionKind.SetSignature, - ] as const; - - return weights.indexOf(a.kind) < weights.indexOf(b.kind); + kind(a, b, { kindSortOrder }) { + return kindSortOrder.indexOf(a.kind) < kindSortOrder.indexOf(b.kind); }, }; -export function sortReflections( - reflections: DeclarationReflection[], - strategies: readonly SortStrategy[] -) { - reflections.sort((a, b) => { - for (const s of strategies) { - if (sorts[s](a, b)) { - return -1; - } - if (sorts[s](b, a)) { - return 1; - } +export function getSortFunction(opts: Options) { + const kindSortOrder = opts + .getValue("kindSortOrder") + .map((k) => ReflectionKind[k]); + + for (const kind of defaultKindSortOrder) { + if (!kindSortOrder.includes(kind)) { + kindSortOrder.push(kind); } - return 0; - }); + } + + const strategies = opts.getValue("sort"); + const data = { kindSortOrder }; + + return function sortReflections(reflections: DeclarationReflection[]) { + reflections.sort((a, b) => { + for (const s of strategies) { + if (sorts[s](a, b, data)) { + return -1; + } + if (sorts[s](b, a, data)) { + return 1; + } + } + return 0; + }); + }; } diff --git a/src/test/utils/enum.test.ts b/src/test/utils/enum.test.ts new file mode 100644 index 000000000..4dfde4052 --- /dev/null +++ b/src/test/utils/enum.test.ts @@ -0,0 +1,11 @@ +import { ok } from "assert"; +import { ReflectionKind } from "../../lib/models"; +import { getEnumKeys } from "../../lib/utils/enum"; + +describe("Enum utils", () => { + it("Should be able to get enum keys", () => { + const keys = getEnumKeys(ReflectionKind); + ok(keys.includes("Project")); + ok(!keys.includes("SignatureContainer")); + }); +}); diff --git a/src/test/utils/sort.test.ts b/src/test/utils/sort.test.ts index d962f6a26..10d5fb369 100644 --- a/src/test/utils/sort.test.ts +++ b/src/test/utils/sort.test.ts @@ -7,9 +7,20 @@ import { ReflectionKind, } from "../../lib/models"; import { resetReflectionID } from "../../lib/models/reflections/abstract"; -import { sortReflections } from "../../lib/utils"; +import { Logger, Options } from "../../lib/utils"; +import { getSortFunction, SortStrategy } from "../../lib/utils/sort"; describe("Sort", () => { + function sortReflections( + arr: DeclarationReflection[], + strategies: SortStrategy[] + ) { + const opts = new Options(new Logger()); + opts.addDefaultDeclarations(); + opts.setValue("sort", strategies); + getSortFunction(opts)(arr); + } + it("Should sort by name", () => { const arr = [ new DeclarationReflection("a", ReflectionKind.TypeAlias),