Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Only generate legend for items that are displayed on the page #1187

Merged
merged 1 commit into from
Mar 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/lib/output/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Event } from '../utils/events';
import { ProjectReflection } from '../models/reflections/project';
import { UrlMapping } from './models/UrlMapping';
import { NavigationItem } from './models/NavigationItem';
import { LegendItem } from './plugins/LegendPlugin';

/**
* An event emitted by the [[Renderer]] class at the very beginning and
Expand Down Expand Up @@ -128,6 +129,11 @@ export class PageEvent extends Event {
*/
toc?: NavigationItem;

/**
* The legend items that are applicable for this page
*/
legend?: LegendItem[][];

/**
* The final html content of this page.
*
Expand Down
192 changes: 192 additions & 0 deletions src/lib/output/plugins/LegendPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { Reflection, DeclarationReflection, ProjectReflection } from '../../models/reflections/index';
import { Component, RendererComponent } from '../components';
import { PageEvent, RendererEvent } from '../events';

export interface LegendItem {
/**
* Legend item name
*/
name: string;

/**
* List of css classes that represent the legend item
*/
classes: string[];
}

const ignoredClasses = [
'tsd-parent-kind-external-module',
'tsd-is-not-exported',
'tsd-is-overwrite'
];

const completeLegend: LegendItem[][] = [
[
{ name: 'Module', classes: ['tsd-kind-module'] },
{ name: 'Object literal', classes: ['tsd-kind-object-literal'] },
{ name: 'Variable', classes: ['tsd-kind-variable'] },
{ name: 'Function', classes: ['tsd-kind-function'] },
{ name: 'Function with type parameter', classes: ['tsd-kind-function', 'tsd-has-type-parameter'] },
{ name: 'Index signature', classes: ['tsd-kind-index-signature'] },
{ name: 'Type alias', classes: ['tsd-kind-type-alias'] },
{ name: 'Type alias with type parameter', classes: ['tsd-kind-type-alias', 'tsd-has-type-parameter'] }
],
[
{ name: 'Enumeration', classes: ['tsd-kind-enum'] },
{ name: 'Enumeration member', classes: ['tsd-kind-enum-member'] },
{ name: 'Property', classes: ['tsd-kind-property', 'tsd-parent-kind-enum'] },
{ name: 'Method', classes: ['tsd-kind-method', 'tsd-parent-kind-enum'] }
],
[
{ name: 'Interface', classes: ['tsd-kind-interface'] },
{ name: 'Interface with type parameter', classes: ['tsd-kind-interface', 'tsd-has-type-parameter'] },
{ name: 'Constructor', classes: ['tsd-kind-constructor', 'tsd-parent-kind-interface'] },
{ name: 'Property', classes: ['tsd-kind-property', 'tsd-parent-kind-interface'] },
{ name: 'Method', classes: ['tsd-kind-method', 'tsd-parent-kind-interface'] },
{ name: 'Index signature', classes: ['tsd-kind-index-signature', 'tsd-parent-kind-interface'] }
],
[
{ name: 'Class', classes: ['tsd-kind-class'] },
{ name: 'Class with type parameter', classes: ['tsd-kind-class', 'tsd-has-type-parameter'] },
{ name: 'Constructor', classes: ['tsd-kind-constructor', 'tsd-parent-kind-class'] },
{ name: 'Property', classes: ['tsd-kind-property', 'tsd-parent-kind-class'] },
{ name: 'Method', classes: ['tsd-kind-method', 'tsd-parent-kind-class'] },
{ name: 'Accessor', classes: ['tsd-kind-accessor', 'tsd-parent-kind-class'] },
{ name: 'Index signature', classes: ['tsd-kind-index-signature', 'tsd-parent-kind-class'] }
],
[
{ name: 'Inherited constructor', classes: ['tsd-kind-constructor', 'tsd-parent-kind-class', 'tsd-is-inherited'] },
{ name: 'Inherited property', classes: ['tsd-kind-property', 'tsd-parent-kind-class', 'tsd-is-inherited'] },
{ name: 'Inherited method', classes: ['tsd-kind-method', 'tsd-parent-kind-class', 'tsd-is-inherited'] },
{ name: 'Inherited accessor', classes: ['tsd-kind-accessor', 'tsd-parent-kind-class', 'tsd-is-inherited'] }
],
[
{ name: 'Protected property', classes: ['tsd-kind-property', 'tsd-parent-kind-class', 'tsd-is-protected'] },
{ name: 'Protected method', classes: ['tsd-kind-method', 'tsd-parent-kind-class', 'tsd-is-protected'] },
{ name: 'Protected accessor', classes: ['tsd-kind-accessor', 'tsd-parent-kind-class', 'tsd-is-protected'] }
],
[
{ name: 'Private property', classes: ['tsd-kind-property', 'tsd-parent-kind-class', 'tsd-is-private'] },
{ name: 'Private method', classes: ['tsd-kind-method', 'tsd-parent-kind-class', 'tsd-is-private'] },
{ name: 'Private accessor', classes: ['tsd-kind-accessor', 'tsd-parent-kind-class', 'tsd-is-private'] }
],
[
{ name: 'Static property', classes: ['tsd-kind-property', 'tsd-parent-kind-class', 'tsd-is-static'] },
{ name: 'Static method', classes: ['tsd-kind-method', 'tsd-parent-kind-class', 'tsd-is-static'] }
]
];

export class LegendBuilder {
private _classesList: Set<string>[];

constructor() {
this._classesList = [];
}

build(): LegendItem[][] {
const filteredLegend = completeLegend.map(list => {
return list.filter(item => {
for (const classes of this._classesList) {
if (this.isArrayEqualToSet(item.classes, classes)) {
return true;
}
}
return false;
});
}).filter(list => list.length);

return filteredLegend;
}

registerCssClasses(classArray: string[]) {
let exists = false;
const items = classArray.filter(css => !ignoredClasses.some(ignored => ignored === css));

for (const classes of this._classesList) {
if (this.isArrayEqualToSet(items, classes)) {
exists = true;
break;
}
}

if (!exists) {
this._classesList.push(new Set(items));
}
}

private isArrayEqualToSet<T>(a: T[], b: Set<T>) {
if (a.length !== b.size) {
return false;
}

for (const value of a) {
if (!b.has(value)) {
return false;
}
}
return true;
}
}

/**
* A plugin that generates the legend for the current page.
*
* This plugin sets the [[PageEvent.legend]] property.
*/
@Component({name: 'legend'})
export class LegendPlugin extends RendererComponent {
private _project!: ProjectReflection;

/**
* Create a new LegendPlugin instance.
*/
initialize() {
this.listenTo(this.owner, {
[RendererEvent.BEGIN]: this.onRenderBegin,
[PageEvent.BEGIN]: this.onRendererBeginPage
});
}

private onRenderBegin(event: RendererEvent) {
this._project = event.project;
}

/**
* Triggered before a document will be rendered.
*
* @param page An event object describing the current render operation.
*/
private onRendererBeginPage(page: PageEvent) {
const model = page.model;
const builder = new LegendBuilder();

// immediate children
this.buildLegend(model, builder);

// top level items (as appears in navigation)
this._project.children?.forEach(reflection => {
if (reflection !== model) {
this.buildLegend(reflection, builder);
}
});

page.legend = builder.build().sort((a, b) => b.length - a.length);
}

private buildLegend(model: Reflection, builder: LegendBuilder) {
if (model instanceof DeclarationReflection) {
const children = (model.children || [] as Array<Reflection | undefined>)
.concat(...model.groups?.map(group => group.children) || [])
.concat(...model.getAllSignatures())
.concat(model.indexSignature as Reflection)
.filter(item => item);

for (const child of children) {
const cssClasses = child?.cssClasses?.split(' ');
if (cssClasses) {
builder.registerCssClasses(cssClasses);
}
}
}
}
}
1 change: 1 addition & 0 deletions src/lib/output/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { MarkedPlugin } from './MarkedPlugin';
export { NavigationPlugin } from './NavigationPlugin';
export { PrettyPrintPlugin } from './PrettyPrintPlugin';
export { TocPlugin } from './TocPlugin';
export { LegendPlugin } from './LegendPlugin';
57 changes: 57 additions & 0 deletions src/test/legend-builder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Assert = require('assert');
import { LegendBuilder } from '../lib/output/plugins/LegendPlugin';

describe('LegendBuilder', function () {
it('returns empty items when no css classes are registered', function () {
const builder = new LegendBuilder();
const results = builder.build().map(items => items.map(item => item.name));

Assert.deepEqual(results, []);
});

it('returns single item list when common css classes are registered', function () {
const builder = new LegendBuilder();
builder.registerCssClasses(['tsd-kind-module']);
const results = builder.build().map(items => items.map(item => item.name));

Assert.deepEqual(results, [['Module']]);
});

it('returns single item list with multiple items when common css classes are registered', function () {
const builder = new LegendBuilder();
builder.registerCssClasses(['tsd-kind-module']);
builder.registerCssClasses(['tsd-kind-function']);
const results = builder.build().map(items => items.map(item => item.name));

Assert.deepEqual(results, [['Module', 'Function']]);
});

it('returns single item list with multiple items when multiple css classes are registered', function () {
const builder = new LegendBuilder();
builder.registerCssClasses(['tsd-kind-module']);
builder.registerCssClasses(['tsd-kind-function']);
builder.registerCssClasses(['tsd-kind-function', 'tsd-has-type-parameter']);
const results = builder.build().map(items => items.map(item => item.name));

Assert.deepEqual(results, [['Module', 'Function', 'Function with type parameter']]);
});

it('returns multiple item list when common css classes are registered from different groups', function () {
const builder = new LegendBuilder();
builder.registerCssClasses(['tsd-kind-module']);
builder.registerCssClasses(['tsd-kind-accessor', 'tsd-parent-kind-class', 'tsd-is-inherited']);
builder.registerCssClasses(['tsd-kind-property', 'tsd-parent-kind-class', 'tsd-is-private']);
builder.registerCssClasses(['tsd-kind-accessor', 'tsd-parent-kind-class', 'tsd-is-private']);
const results = builder.build().map(items => items.map(item => item.name));

Assert.deepEqual(results, [['Module'], ['Inherited accessor'], ['Private property', 'Private accessor']]);
});

it('returns single item when includes ignored classes', function () {
const builder = new LegendBuilder();
builder.registerCssClasses(['tsd-kind-class', 'tsd-parent-kind-external-module']);
const results = builder.build().map(items => items.map(item => item.name));

Assert.deepEqual(results, [['Class']]);
});
});