Skip to content

Commit

Permalink
Added support for custom filters based on modifier tags
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit0 committed Apr 19, 2022
1 parent 8ff6b4c commit 3dd2159
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 77 deletions.
10 changes: 8 additions & 2 deletions .config/typedoc.json
Expand Up @@ -12,13 +12,19 @@
"entryPoints": ["../src"],
"entryPointStrategy": "Resolve",
"excludeExternals": true,
"excludePrivate": false,
"excludeInternal": true,
"excludeInternal": false,
"treatWarningsAsErrors": false,
"validation": {
"notExported": true,
"invalidLink": true,
"notDocumented": false
},
"visibilityFilters": {
"protected": false,
"private": false,
"inherited": true,
"external": false,
"@internal": false
},
"logLevel": "Verbose"
}
3 changes: 1 addition & 2 deletions CHANGELOG.md
Expand Up @@ -9,8 +9,6 @@ These TODOs will be resolved before a full release. ([GitHub project](https://gi
- Full support for declaration references, #262, #488, #1326, #1845.
- Add support for additional comment styles, #1433.
- Theme: Custom rendering for `@see` tags.
- Theme: Show toggles for all modifier tags used in a project to allow users to filter by deprecated/alpha/beta, etc.
- Add option to control default values (merge #1816. Same option? Different one since it's based on comments?)

### Breaking Changes

Expand Down Expand Up @@ -57,6 +55,7 @@ These TODOs will be resolved before a full release. ([GitHub project](https://gi
- Correctly detect optional parameters in JavaScript projects using JSDoc, #1804.
- Fixed identical anchor links for reflections with the same name, #1845.
- JS exports defined as `exports.foo = ...` will now be converted as variables rather than properties.
- Corrected schema generation for https://typedoc.org/schema.json

### Thanks!

Expand Down
32 changes: 30 additions & 2 deletions scripts/generate_options_schema.js
Expand Up @@ -24,8 +24,12 @@ addTypeDocOptions({
description: option.help,
};

switch (option.type ?? ParameterType.String) {
const type = option.type ?? ParameterType.String;
switch (type) {
case ParameterType.Array:
case ParameterType.GlobArray:
case ParameterType.PathArray:
case ParameterType.ModuleArray:
data.type = "array";
data.items = { type: "string" };
data.default =
Expand All @@ -34,6 +38,7 @@ addTypeDocOptions({
).defaultValue ?? [];
break;
case ParameterType.String:
case ParameterType.Path:
data.type = "string";
if (!IGNORED_DEFAULT_OPTIONS.has(option.name)) {
data.default =
Expand Down Expand Up @@ -105,7 +110,15 @@ addTypeDocOptions({
data.default = defaults;
}
case ParameterType.Mixed:
break; // Nothing to do... TypeDoc really shouldn't have any of these.
data.default =
/** @type {import("../dist").MixedDeclarationOption} */ (
option
).defaultValue;
break;

default:
/** @type {never} */
let _unused = type;
}

schema.properties[option.name] = data;
Expand All @@ -115,6 +128,21 @@ addTypeDocOptions({
schema.properties.logger.enum = ["console", "none"];
schema.properties.logger.default = "console";

schema.properties.visibilityFilters.type = "object";
schema.properties.visibilityFilters.properties = Object.fromEntries(
Object.keys(schema.properties.visibilityFilters.default).map((x) => [
x,
{ type: "boolean" },
])
);
schema.properties.visibilityFilters.patternProperties = {
"^@": { type: "boolean" },
};
schema.properties.visibilityFilters.additionalProperties = false;

schema.properties.compilerOptions.type = "object";
schema.properties.compilerOptions.markedOptions = "object";

const output = JSON.stringify(schema, null, "\t");

if (process.argv.length > 2) {
Expand Down
36 changes: 22 additions & 14 deletions src/lib/output/themes/default/DefaultTheme.tsx
Expand Up @@ -150,9 +150,10 @@ export class DefaultTheme extends Theme {
* @param event An event object describing the current render operation.
*/
private onRendererBegin(event: RendererEvent) {
const filters = this.application.options.getValue("visibilityFilters") as Record<string, boolean>;
for (const reflection of Object.values(event.project.reflections)) {
if (reflection instanceof DeclarationReflection) {
DefaultTheme.applyReflectionClasses(reflection);
DefaultTheme.applyReflectionClasses(reflection, filters);
}
}
}
Expand Down Expand Up @@ -254,7 +255,7 @@ export class DefaultTheme extends Theme {
*
* @param reflection The reflection whose cssClasses property should be generated.
*/
static applyReflectionClasses(reflection: DeclarationReflection) {
static applyReflectionClasses(reflection: DeclarationReflection, filters: Record<string, boolean>) {
const classes: string[] = [];

classes.push(DefaultTheme.toStyleClass("tsd-kind-" + ReflectionKind[reflection.kind]));
Expand All @@ -265,25 +266,32 @@ export class DefaultTheme extends Theme {

// Filter classes should match up with the settings function in
// partials/navigation.tsx.
if (reflection.inheritedFrom) {
classes.push("tsd-is-inherited");
}
if (reflection.flags.isPrivate) {
classes.push("tsd-is-private");
}
if (reflection.flags.isProtected) {
classes.push("tsd-is-protected");
}
if (reflection.flags.isExternal) {
classes.push("tsd-is-external");
for (const key of Object.keys(filters)) {
if (key === "inherited") {
if (reflection.inheritedFrom) {
classes.push("tsd-is-inherited");
}
} else if (key === "protected") {
if (reflection.flags.isProtected) {
classes.push("tsd-is-protected");
}
} else if (key === "external") {
if (reflection.flags.isExternal) {
classes.push("tsd-is-external");
}
} else if (key.startsWith("@")) {
if (reflection.comment?.hasModifier(key as `@${string}`)) {
classes.push(DefaultTheme.toStyleClass(`tsd-is-${key.substring(1)}`));
}
}
}

reflection.cssClasses = classes.join(" ");
}

/**
* Transform a space separated string into a string suitable to be used as a
* css class, e.g. "constructor method" > "Constructor-method".
* css class, e.g. "constructor method" > "constructor-method".
*/
static toStyleClass(str: string) {
return str.replace(/(\w)([A-Z])/g, (_m, m1, m2) => m1 + "-" + m2).toLowerCase();
Expand Down
7 changes: 2 additions & 5 deletions src/lib/output/themes/default/partials/comment.tsx
@@ -1,10 +1,7 @@
import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext";
import { assertNever, JSX, Raw } from "../../../../utils";
import type { CommentDisplayPart, Reflection } from "../../../../models";

function humanize(text: string) {
return text.substring(1, 2).toUpperCase() + text.substring(2).replace(/[a-z][A-Z]/g, (x) => `${x[0]} ${x[1]}`);
}
import { camelToTitleCase } from "../../lib";

function displayPartsToMarkdown(parts: CommentDisplayPart[], urlTo: DefaultThemeRenderContext["urlTo"]) {
const result: string[] = [];
Expand Down Expand Up @@ -57,7 +54,7 @@ export function comment({ markdown, urlTo }: DefaultThemeRenderContext, props: R
<Raw html={markdown(displayPartsToMarkdown(props.comment.summary, urlTo))} />
{props.comment.blockTags.map((item) => (
<>
<h3>{humanize(item.tag)}</h3>
<h3>{camelToTitleCase(item.tag.substring(1))}</h3>
<Raw html={markdown(displayPartsToMarkdown(item.content, urlTo))} />
</>
))}
Expand Down
93 changes: 49 additions & 44 deletions src/lib/output/themes/default/partials/navigation.tsx
@@ -1,7 +1,7 @@
import { ContainerReflection, DeclarationReflection, Reflection, ReflectionKind } from "../../../../models";
import { JSX, partition } from "../../../../utils";
import type { PageEvent } from "../../../events";
import { classNames, wbr } from "../../lib";
import { classNames, camelToTitleCase, wbr } from "../../lib";
import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext";
import { icons } from "./icon";

Expand All @@ -15,64 +15,69 @@ export function navigation(context: DefaultThemeRenderContext, props: PageEvent<
);
}

function buildFilterItem(name: string, displayName: string, defaultValue: boolean) {
return (
<li class="tsd-filter-item">
<label class="tsd-filter-input">
<input type="checkbox" id={`tsd-filter-${name}`} name={name} checked={defaultValue} />
{icons.checkbox()}
<span>{displayName}</span>
</label>
</li>
);
}

function settings(context: DefaultThemeRenderContext) {
const defaultFilters = context.options.getValue("visibilityFilters");
const defaultFilters = context.options.getValue("visibilityFilters") as Record<string, boolean>;

const filters: Array<keyof typeof defaultFilters> = [];
if (!context.options.getValue("excludeProtected")) {
filters.push("protected");
}
if (!context.options.getValue("excludePrivate")) {
filters.push("private");
}
if (!context.options.getValue("excludeExternals")) {
filters.push("external");
const visibilityOptions: JSX.Element[] = [];

for (const key of Object.keys(defaultFilters)) {
if (key.startsWith("@")) {
const filterName = key
.substring(1)
.replace(/([a-z])([A-Z])/g, "$1-$2")
.toLowerCase();

visibilityOptions.push(
buildFilterItem(filterName, camelToTitleCase(key.substring(1)), defaultFilters[key])
);
} else if (
(key === "protected" && !context.options.getValue("excludeProtected")) ||
(key === "private" && !context.options.getValue("excludePrivate")) ||
(key === "external" && !context.options.getValue("excludeExternals")) ||
key === "inherited"
) {
visibilityOptions.push(buildFilterItem(key, camelToTitleCase(key), defaultFilters[key]));
}
}
filters.push("inherited");

// Settings panel above navigation

const visibilityOptions = filters.map((name) => {
const value = name.charAt(0).toUpperCase() + name.slice(1);
return (
<li class="tsd-filter-item">
<label class="tsd-filter-input">
<input
type="checkbox"
id={`tsd-filter-${name}`}
name={name}
value={value}
checked={defaultFilters[name]}
/>
{icons.checkbox()}
<span>{value}</span>
</label>
</li>
);
});

return (
<div class="tsd-navigation settings">
<details class="tsd-index-accordion" open={false}>
<summary class="tsd-accordion-summary">
<h3>{icons.chevronDown()} Settings</h3>
</summary>
<div class="tsd-accordion-details">
<div class="tsd-filter-visibility">
<h4 class="uppercase">Member Visibility</h4>
<form>
<ul id="tsd-filter-options">{...visibilityOptions}</ul>
</form>
{visibilityOptions.length && (
<div class="tsd-filter-visibility">
<h4 class="uppercase">Member Visibility</h4>
<form>
<ul id="tsd-filter-options">{...visibilityOptions}</ul>
</form>
</div>
)}
<div class="tsd-theme-toggle">
<h4 class="uppercase">Theme</h4>
<select id="theme">
<option value="os">OS</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
<div class="tsd-theme-toggle">
<h4 class="uppercase">Theme</h4>
<select id="theme">
<option value="os">OS</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</details>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions src/lib/output/themes/lib.tsx
Expand Up @@ -107,3 +107,7 @@ export function renderTypeParametersSignature(
</>
);
}

export function camelToTitleCase(text: string) {
return text.substring(0, 1).toUpperCase() + text.substring(1).replace(/[a-z][A-Z]/g, (x) => `${x[0]} ${x[1]}`);
}
7 changes: 1 addition & 6 deletions src/lib/utils/options/declaration.ts
Expand Up @@ -82,12 +82,7 @@ export interface TypeDocOptionMap {
lightHighlightTheme: ShikiTheme;
darkHighlightTheme: ShikiTheme;
customCss: string;
visibilityFilters: {
private: boolean;
protected: boolean;
inherited: boolean;
external: boolean;
};
visibilityFilters: unknown;

name: string;
includeVersion: boolean;
Expand Down
26 changes: 24 additions & 2 deletions src/lib/utils/options/sources/typedoc.ts
Expand Up @@ -37,13 +37,35 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
options.addDeclaration({
name: "visibilityFilters",
help: "Specify the default visibility for builtin filters and additional filters according to modifier tags.",
type: ParameterType.Flags,
defaults: {
type: ParameterType.Mixed,
defaultValue: {
protected: false,
private: false,
inherited: true,
external: false,
},
validate(value) {
const knownKeys = ["protected", "private", "inherited", "external"];
if (!value || typeof value !== "object") {
throw new Error("visibilityFilters must be an object.");
}

for (const [key, val] of Object.entries(value)) {
if (!key.startsWith("@") && !knownKeys.includes(key)) {
throw new Error(
`visibilityFilters can only include the following non-@ keys: ${knownKeys.join(
", "
)}`
);
}

if (typeof val !== "boolean") {
throw new Error(
`All values of visibilityFilters must be booleans.`
);
}
}
},
});

options.addDeclaration({
Expand Down

0 comments on commit 3dd2159

Please sign in to comment.