Skip to content

Commit e125484

Browse files
committed
feat: Add support for sorting reflections based on user criteria
Resolves #112
1 parent d5bb930 commit e125484

File tree

14 files changed

+505
-124
lines changed

14 files changed

+505
-124
lines changed

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export {
2121
TSConfigReader,
2222
TypeDocReader,
2323
ArgumentsReader,
24-
} from "./lib/utils/options";
24+
} from "./lib/utils";
2525

2626
export type {
2727
OptionsReader,
@@ -37,7 +37,8 @@ export type {
3737
MixedDeclarationOption,
3838
MapDeclarationOption,
3939
DeclarationOptionToOptionType,
40-
} from "./lib/utils/options";
40+
SortStrategy,
41+
} from "./lib/utils";
4142

4243
export { JSONOutput } from "./lib/serialization";
4344

src/lib/application.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,10 @@ export class Application extends ChildableComponent<
141141
}
142142
this.logger.level = this.options.getValue("logLevel");
143143

144-
let plugins = this.options.getValue("plugin");
145-
if (plugins.length === 0) {
146-
plugins = discoverNpmPlugins(this);
147-
}
148-
loadPlugins(this, this.options.getValue("plugin"));
144+
const plugins = this.options.isSet("plugin")
145+
? this.options.getValue("plugin")
146+
: discoverNpmPlugins(this);
147+
loadPlugins(this, plugins);
149148

150149
this.options.reset();
151150
for (const [key, val] of Object.entries(options)) {

src/lib/converter/plugins/GroupPlugin.ts

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SourceDirectory } from "../../models/sources/directory";
99
import { Component, ConverterComponent } from "../components";
1010
import { Converter } from "../converter";
1111
import { Context } from "../context";
12+
import { sortReflections } from "../../utils/sort";
1213

1314
/**
1415
* A handler that sorts and groups the found reflections in the resolving phase.
@@ -17,38 +18,6 @@ import { Context } from "../context";
1718
*/
1819
@Component({ name: "group" })
1920
export class GroupPlugin extends ConverterComponent {
20-
/**
21-
* Define the sort order of reflections.
22-
*/
23-
static WEIGHTS = [
24-
ReflectionKind.Project,
25-
ReflectionKind.Module,
26-
ReflectionKind.Namespace,
27-
ReflectionKind.Enum,
28-
ReflectionKind.EnumMember,
29-
ReflectionKind.Class,
30-
ReflectionKind.Interface,
31-
ReflectionKind.TypeAlias,
32-
33-
ReflectionKind.Constructor,
34-
ReflectionKind.Event,
35-
ReflectionKind.Property,
36-
ReflectionKind.Variable,
37-
ReflectionKind.Function,
38-
ReflectionKind.Accessor,
39-
ReflectionKind.Method,
40-
ReflectionKind.ObjectLiteral,
41-
42-
ReflectionKind.Parameter,
43-
ReflectionKind.TypeParameter,
44-
ReflectionKind.TypeLiteral,
45-
ReflectionKind.CallSignature,
46-
ReflectionKind.ConstructorSignature,
47-
ReflectionKind.IndexSignature,
48-
ReflectionKind.GetSignature,
49-
ReflectionKind.SetSignature,
50-
];
51-
5221
/**
5322
* Define the singular name of individual reflection kinds.
5423
*/
@@ -123,7 +92,10 @@ export class GroupPlugin extends ConverterComponent {
12392
reflection.children.length > 0 &&
12493
!reflection.groups
12594
) {
126-
reflection.children.sort(GroupPlugin.sortCallback);
95+
sortReflections(
96+
reflection.children,
97+
this.application.options.getValue("sort")
98+
);
12799
reflection.groups = GroupPlugin.getReflectionGroups(
128100
reflection.children
129101
);
@@ -234,30 +206,4 @@ export class GroupPlugin extends ConverterComponent {
234206
return this.getKindString(kind) + "s";
235207
}
236208
}
237-
238-
/**
239-
* Callback used to sort reflections by weight defined by ´GroupPlugin.WEIGHTS´ and name.
240-
*
241-
* @param a The left reflection to sort.
242-
* @param b The right reflection to sort.
243-
* @returns The sorting weight.
244-
*/
245-
static sortCallback(a: Reflection, b: Reflection): number {
246-
const aWeight = GroupPlugin.WEIGHTS.indexOf(a.kind);
247-
const bWeight = GroupPlugin.WEIGHTS.indexOf(b.kind);
248-
if (aWeight === bWeight) {
249-
if (a.flags.isStatic && !b.flags.isStatic) {
250-
return 1;
251-
}
252-
if (!a.flags.isStatic && b.flags.isStatic) {
253-
return -1;
254-
}
255-
if (a.name === b.name) {
256-
return 0;
257-
}
258-
return a.name > b.name ? 1 : -1;
259-
} else {
260-
return aWeight - bWeight;
261-
}
262-
}
263209
}

src/lib/models/reflections/abstract.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ok } from "assert";
12
import { SourceReference } from "../sources/file";
23
import { Type } from "../types/index";
34
import { Comment } from "../comments/comment";
@@ -366,6 +367,15 @@ export abstract class Reflection {
366367
*/
367368
parent?: Reflection;
368369

370+
get project(): ProjectReflection {
371+
if (this.isProject()) return this;
372+
ok(
373+
this.parent,
374+
"Tried to get the project on a reflection not in a project"
375+
);
376+
return this.parent.project;
377+
}
378+
369379
/**
370380
* The parsed documentation comment attached to this reflection.
371381
*/

src/lib/utils/index.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
export type { IfInternal, NeverIfInternal } from "./general";
22

3-
export { Options, ParameterType, ParameterHint, BindOption } from "./options";
3+
export {
4+
Options,
5+
ParameterType,
6+
ParameterHint,
7+
BindOption,
8+
TSConfigReader,
9+
TypeDocReader,
10+
ArgumentsReader,
11+
} from "./options";
12+
export type {
13+
OptionsReader,
14+
TypeDocOptions,
15+
TypeDocOptionMap,
16+
KeyToDeclaration,
17+
DeclarationOption,
18+
DeclarationOptionBase,
19+
StringDeclarationOption,
20+
NumberDeclarationOption,
21+
BooleanDeclarationOption,
22+
ArrayDeclarationOption,
23+
MixedDeclarationOption,
24+
MapDeclarationOption,
25+
DeclarationOptionToOptionType,
26+
} from "./options";
27+
428
export {
529
insertPrioritySorted,
630
removeIfPresent,
@@ -23,3 +47,6 @@ export {
2347
} from "./fs";
2448
export { Logger, LogLevel, ConsoleLogger, CallbackLogger } from "./loggers";
2549
export { loadPlugins, discoverNpmPlugins } from "./plugins";
50+
51+
export { sortReflections } from "./sort";
52+
export type { SortStrategy } from "./sort";

src/lib/utils/options/declaration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Theme as ShikiTheme } from "shiki";
22
import { LogLevel } from "../loggers";
3+
import { SortStrategy } from "../sort";
34

45
/**
56
* An interface describing all TypeDoc specific options. Generated from a
@@ -66,6 +67,7 @@ export interface TypeDocOptionMap {
6667
defaultCategory: string;
6768
categoryOrder: string[];
6869
categorizeByGroup: boolean;
70+
sort: SortStrategy[];
6971
gitRevision: string;
7072
gitRemote: string;
7173
gaID: string;

src/lib/utils/options/sources/typedoc.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Options } from "..";
22
import { LogLevel } from "../../loggers";
33
import { ParameterType, ParameterHint } from "../declaration";
44
import { BUNDLED_THEMES } from "shiki";
5+
import { SORT_STRATEGIES } from "../../sort";
56

67
export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
78
options.addDeclaration({
@@ -188,6 +189,30 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
188189
type: ParameterType.Boolean,
189190
defaultValue: true,
190191
});
192+
options.addDeclaration({
193+
name: "sort",
194+
help: "Specify the sort strategy for documented values",
195+
type: ParameterType.Array,
196+
defaultValue: ["kind", "instance-first", "alphabetical"],
197+
validate(value) {
198+
const invalid = new Set(value);
199+
for (const v of SORT_STRATEGIES) {
200+
invalid.delete(v);
201+
}
202+
203+
if (invalid.size !== 0) {
204+
throw new Error(
205+
`sort may only specify known values, and invalid values were provided (${Array.from(
206+
invalid
207+
).join(
208+
", "
209+
)}). The valid sort strategies are:\n${SORT_STRATEGIES.join(
210+
", "
211+
)}`
212+
);
213+
}
214+
},
215+
});
191216
options.addDeclaration({
192217
name: "gitRevision",
193218
help:

src/lib/utils/sort.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* Module which handles sorting reflections according to a user specified strategy.
3+
* @module
4+
*/
5+
6+
import { DeclarationReflection, ReflectionKind } from "../models";
7+
8+
export const SORT_STRATEGIES = [
9+
"source-order",
10+
"alphabetical",
11+
"enum-value-ascending",
12+
"enum-value-descending",
13+
"static-first",
14+
"instance-first",
15+
"visibility",
16+
"required-first",
17+
"kind",
18+
] as const;
19+
20+
export type SortStrategy = typeof SORT_STRATEGIES[number];
21+
22+
// Return true if a < b
23+
const sorts: Record<
24+
SortStrategy,
25+
(a: DeclarationReflection, b: DeclarationReflection) => boolean
26+
> = {
27+
"source-order"(a, b) {
28+
const aSymbol = a.project.getSymbolFromReflection(a);
29+
const bSymbol = b.project.getSymbolFromReflection(b);
30+
31+
// This is going to be somewhat ambiguous. No way around that. Treat the first
32+
// declaration of a symbol as its ordering declaration.
33+
const aDecl = aSymbol?.getDeclarations()?.[0];
34+
const bDecl = bSymbol?.getDeclarations()?.[0];
35+
36+
if (aDecl && bDecl) {
37+
const aFile = aDecl.getSourceFile().fileName;
38+
const bFile = bDecl.getSourceFile().fileName;
39+
if (aFile < bFile) {
40+
return true;
41+
}
42+
if (aFile == bFile && aDecl.pos < bDecl.pos) {
43+
return true;
44+
}
45+
46+
return false;
47+
}
48+
49+
// Someone is doing something weird. Fail to re-order. This *might* be a bug in TD
50+
// but it could also be TS having some exported symbol without a declaration.
51+
return false;
52+
},
53+
alphabetical(a, b) {
54+
return a.name < b.name;
55+
},
56+
"enum-value-ascending"(a, b) {
57+
if (
58+
a.kind == ReflectionKind.EnumMember &&
59+
b.kind == ReflectionKind.EnumMember
60+
) {
61+
return (
62+
parseFloat(a.defaultValue ?? "0") <
63+
parseFloat(b.defaultValue ?? "0")
64+
);
65+
}
66+
return false;
67+
},
68+
"enum-value-descending"(a, b) {
69+
if (
70+
a.kind == ReflectionKind.EnumMember &&
71+
b.kind == ReflectionKind.EnumMember
72+
) {
73+
return (
74+
parseFloat(b.defaultValue ?? "0") <
75+
parseFloat(a.defaultValue ?? "0")
76+
);
77+
}
78+
return false;
79+
},
80+
"static-first"(a, b) {
81+
return a.flags.isStatic && !b.flags.isStatic;
82+
},
83+
"instance-first"(a, b) {
84+
return !a.flags.isStatic && b.flags.isStatic;
85+
},
86+
visibility(a, b) {
87+
// Note: flags.isPublic may not be set on public members. It will only be set
88+
// if the user explicitly marks members as public. Therefore, we can't use it
89+
// here to get a reliable sort order.
90+
if (a.flags.isPrivate) {
91+
return false; // Not sorted before anything
92+
}
93+
if (a.flags.isProtected) {
94+
return b.flags.isPrivate; // Sorted before privates
95+
}
96+
if (b.flags.isPrivate || b.flags.isProtected) {
97+
return true; // We are public, sort before b if b is less visible
98+
}
99+
return false;
100+
},
101+
"required-first"(a, b) {
102+
return !a.flags.isOptional && b.flags.isOptional;
103+
},
104+
kind(a, b) {
105+
const weights = [
106+
ReflectionKind.Reference,
107+
ReflectionKind.Project,
108+
ReflectionKind.Module,
109+
ReflectionKind.Namespace,
110+
ReflectionKind.Enum,
111+
ReflectionKind.EnumMember,
112+
ReflectionKind.Class,
113+
ReflectionKind.Interface,
114+
ReflectionKind.TypeAlias,
115+
116+
ReflectionKind.Constructor,
117+
ReflectionKind.Event,
118+
ReflectionKind.Property,
119+
ReflectionKind.Variable,
120+
ReflectionKind.Function,
121+
ReflectionKind.Accessor,
122+
ReflectionKind.Method,
123+
ReflectionKind.ObjectLiteral,
124+
125+
ReflectionKind.Parameter,
126+
ReflectionKind.TypeParameter,
127+
ReflectionKind.TypeLiteral,
128+
ReflectionKind.CallSignature,
129+
ReflectionKind.ConstructorSignature,
130+
ReflectionKind.IndexSignature,
131+
ReflectionKind.GetSignature,
132+
ReflectionKind.SetSignature,
133+
] as const;
134+
135+
return weights.indexOf(a.kind) < weights.indexOf(b.kind);
136+
},
137+
};
138+
139+
export function sortReflections(
140+
strategies: DeclarationReflection[],
141+
strats: readonly SortStrategy[]
142+
) {
143+
strategies.sort((a, b) => {
144+
for (const s of strats) {
145+
if (sorts[s](a, b)) {
146+
return -1;
147+
}
148+
if (sorts[s](b, a)) {
149+
return 1;
150+
}
151+
}
152+
return 0;
153+
});
154+
}

0 commit comments

Comments
 (0)