From 923c55dd9d44c14d019a5184edac3dcb440823cb Mon Sep 17 00:00:00 2001 From: Soc Sieng Date: Sun, 15 Mar 2020 11:25:22 -0700 Subject: [PATCH] feat: Only generate legend for items that are displayed on the page (#1187) Closes #1136 --- src/lib/output/events.ts | 6 + src/lib/output/plugins/LegendPlugin.ts | 192 +++++++++++++++++++++++++ src/lib/output/plugins/index.ts | 1 + src/test/legend-builder.test.ts | 57 ++++++++ 4 files changed, 256 insertions(+) create mode 100644 src/lib/output/plugins/LegendPlugin.ts create mode 100644 src/test/legend-builder.test.ts diff --git a/src/lib/output/events.ts b/src/lib/output/events.ts index 95d9abc48..3c68f8049 100644 --- a/src/lib/output/events.ts +++ b/src/lib/output/events.ts @@ -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 @@ -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. * diff --git a/src/lib/output/plugins/LegendPlugin.ts b/src/lib/output/plugins/LegendPlugin.ts new file mode 100644 index 000000000..1dc848a78 --- /dev/null +++ b/src/lib/output/plugins/LegendPlugin.ts @@ -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[]; + + 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(a: T[], b: Set) { + 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) + .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); + } + } + } + } +} diff --git a/src/lib/output/plugins/index.ts b/src/lib/output/plugins/index.ts index 0087c0495..555011938 100644 --- a/src/lib/output/plugins/index.ts +++ b/src/lib/output/plugins/index.ts @@ -6,3 +6,4 @@ export { MarkedPlugin } from './MarkedPlugin'; export { NavigationPlugin } from './NavigationPlugin'; export { PrettyPrintPlugin } from './PrettyPrintPlugin'; export { TocPlugin } from './TocPlugin'; +export { LegendPlugin } from './LegendPlugin'; diff --git a/src/test/legend-builder.test.ts b/src/test/legend-builder.test.ts new file mode 100644 index 000000000..1c25d931f --- /dev/null +++ b/src/test/legend-builder.test.ts @@ -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']]); + }); +});