Skip to content

Commit

Permalink
feat: Only generate legend for items that are displayed on the page (#…
Browse files Browse the repository at this point in the history
…1187)

Closes #1136
  • Loading branch information
socsieng authored Mar 15, 2020
1 parent 592e0b3 commit 923c55d
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 0 deletions.
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']]);
});
});

0 comments on commit 923c55d

Please sign in to comment.