diff --git a/README.md b/README.md index e6370b10..e08aa982 100644 --- a/README.md +++ b/README.md @@ -109,15 +109,15 @@ apexdocs-generate The CLI supports the following parameters: -| Parameter | Alias | Description | Default | Required | -| ----------------- | ----- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------- | -------- | -| --sourceDir | -s | The directory location which contains your apex .cls classes. | N/A | Yes | -| --targetDir | -t | The directory location where documentation will be generated to. | `docs` | No | -| --recursive | -r | Whether .cls classes will be searched for recursively in the directory provided. | `true` | No | -| --scope | -p | A list of scopes to document. Values should be separated by a space, e.g --scope public private | `global namespaceaccessible public` | No | -| --targetGenerator | -g | Define the static file generator for which the documents will be created. Currently supports jekyll and docsify. | `jekyll` | No | -| --configPath | -c | The path to the JSON configuration file that defines the structure of the documents to docGenerator. | N/A | No | -| --group | -o | Define whether the generated files should be grouped by the @group tag on the top level classes. | `true` | No | +| Parameter | Alias | Description | Default | Required | +| ----------------- | ----- |--------------------------------------------------------------------------------------------------------------------------| ----------------------------------- | -------- | +| --sourceDir | -s | The directory location which contains your apex .cls classes. | N/A | Yes | +| --targetDir | -t | The directory location where documentation will be generated to. | `docs` | No | +| --recursive | -r | Whether .cls classes will be searched for recursively in the directory provided. | `true` | No | +| --scope | -p | A list of scopes to document. Values should be separated by a space, e.g --scope public private | `global namespaceaccessible public` | No | +| --targetGenerator | -g | Define the static file generator for which the documents will be created. Currently supports jekyll and docsify. | `jekyll` | No | +| --configPath | -c | (Only versions 1.X) The path to the JSON configuration file that defines the structure of the documents to docGenerator. | N/A | No | +| --group | -o | (Only versions 1.X) Define whether the generated files should be grouped by the @group tag on the top level classes. | `true` | No | #### Configuration File @@ -314,6 +314,34 @@ The following tags are supported on the method level: public static Object call(String action) { ``` +### Grouping Declarations Within A Class + +A class might have members that should be grouped together. For example, you can have a class for constants with +groups of constants that should be grouped together because they share a common behavior (e.g. different groups +of constants representing the possible values for different picklists.) + +You can group things together within a class by using the following syntax: +```apex +// @start-group Group Name or Description +public static final String CONSTANT_FOO = 'Foo'; +public static final String CONSTANT_BAR = 'Bar'; +// @end-group +``` + +Groups of members are displayed together under their own subsection after its name or description. + +Some notes about grouping: +* This is only supported on classes, NOT enums and interfaces +* Supports + * Properties + * Fields (variables and constants) + * Constructors + * Methods +* BUT only members of the same type are grouped together. For example, +if you have a group that contains properties and methods the properties will be grouped together under Properties -> Group Name, and the methods will be grouped together under Methods -> Group Name +* Does not support inner types (inner classes, interfaces, and enums) +* It is necessary to use `// @end-group` whenever a group has been started, otherwise a parsing error will be raised for that file. + ### Inline linking Apexdocs allows you to reference other classes from anywhere in your docs, and automatically creates a link to that diff --git a/docs/Sample-Classes/SampleClass.md b/docs/Sample-Classes/SampleClass.md index d1578135..dd084747 100644 --- a/docs/Sample-Classes/SampleClass.md +++ b/docs/Sample-Classes/SampleClass.md @@ -17,13 +17,14 @@ This is a class description. This class relates to [SampleInterface](/Sample-Int **See** [SampleInterface](/Sample-Interfaces/SampleInterface.md) ## Constructors -### `SampleClass()` +### My Super Group +##### `SampleClass()` `NAMESPACEACCESSIBLE` Constructs a SampleClass without any arguments. This relates to [SampleInterface](/Sample-Interfaces/SampleInterface.md) -#### Throws +###### Throws |Exception|Description| |---|---| |`ExcName`|some exception| @@ -33,41 +34,45 @@ Constructs a SampleClass without any arguments. This relates to [SampleInterface **See** [SampleInterface](/Sample-Interfaces/SampleInterface.md) -#### Example +###### Example ```apex // Example SampleClass sampleInstance = new SampleClass(); ``` -### `SampleClass(String argument)` +--- +### Other +##### `SampleClass(String argument)` Constructs a SampleClass with an argument. -#### Parameters +###### Parameters |Param|Description| |---|---| |`argument`|Argument definition| --- ## Fields +### Common Constants -### `A_CONSTANT` → `String` - -This is a constant. - -### `someVariable` → `String` +* `ANOTHER_CONSTANT` → `String` +* `A_CONSTANT` → `String` [`NAMESPACEACCESSIBLE` ] - This is a constant. +--- +### Other variables +* `someVariable` → `String` --- ## Properties ### `AnotherProp` → `Decimal` -`AURAENABLED` +`AURAENABLED` This is a Decimal property. ### `MyProp` → `String` -`AURAENABLED` +`AURAENABLED` +`DEPRECATED` This is a String property. @@ -122,6 +127,7 @@ Inner class belonging to SampleClass. ##### `InnerProp` → `String` + Description of the inner property. --- @@ -140,6 +146,7 @@ Inner class belonging to SampleClass. ##### `InnerProp` → `String` + Description of the inner property. --- diff --git a/examples/force-app/main/default/classes/SampleClass.cls b/examples/force-app/main/default/classes/SampleClass.cls index d4fe124a..c877c6fb 100644 --- a/examples/force-app/main/default/classes/SampleClass.cls +++ b/examples/force-app/main/default/classes/SampleClass.cls @@ -19,12 +19,20 @@ public with sharing class SampleClass { C } + // @start-group Common Constants /** * @description This is a constant. */ + @NamespaceAccessible public static final String A_CONSTANT = 'My Constant Value'; + public static final String ANOTHER_CONSTANT = 'My Constant Value'; + // @end-group + + // @start-group Other variables public String someVariable = 'test'; + // @end-group + // @start-group My Super Group /** * @description Constructs a SampleClass without any arguments. This relates to {@link SampleInterface} * @throws ExcName some exception @@ -38,6 +46,7 @@ public with sharing class SampleClass { public SampleClass() { System.debug('Constructor'); } + // @end-group /** * @description Constructs a SampleClass with an argument. @@ -73,6 +82,7 @@ public with sharing class SampleClass { * @description This is a String property. */ @AuraEnabled + @Deprecated public String MyProp { get; set; } /** diff --git a/package-lock.json b/package-lock.json index f14c97d6..9a23ad9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@cparra/apexdocs", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cparra/apexdocs", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", "dependencies": { - "@cparra/apex-reflection": "^1.0.0", + "@cparra/apex-reflection": "^1.1.1", "chalk": "^4.1.2", "html-entities": "^2.3.2", "yargs": "^16.0.3" @@ -545,9 +545,9 @@ "dev": true }, "node_modules/@cparra/apex-reflection": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@cparra/apex-reflection/-/apex-reflection-1.0.0.tgz", - "integrity": "sha512-ImuDDyuDZER4VyghfEgs21+RVlu6prFxKJAoDR/wn7+Hg0o4CRsmXbfdYu3iLKkduOC3y61nIbKUgjUwAvSarg==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@cparra/apex-reflection/-/apex-reflection-1.1.1.tgz", + "integrity": "sha512-D+JYpi2D9uZS2baJ/TyulHvN9XtCLNjhiqyf9681TPeViSH9UKoORIkhiQJRdIFf+kFY/41QhvWYoNkUMO+ZYg==" }, "node_modules/@eslint/eslintrc": { "version": "1.2.1", @@ -6551,9 +6551,9 @@ "dev": true }, "@cparra/apex-reflection": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@cparra/apex-reflection/-/apex-reflection-1.0.0.tgz", - "integrity": "sha512-ImuDDyuDZER4VyghfEgs21+RVlu6prFxKJAoDR/wn7+Hg0o4CRsmXbfdYu3iLKkduOC3y61nIbKUgjUwAvSarg==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@cparra/apex-reflection/-/apex-reflection-1.1.1.tgz", + "integrity": "sha512-D+JYpi2D9uZS2baJ/TyulHvN9XtCLNjhiqyf9681TPeViSH9UKoORIkhiQJRdIFf+kFY/41QhvWYoNkUMO+ZYg==" }, "@eslint/eslintrc": { "version": "1.2.1", diff --git a/package.json b/package.json index 50113648..72016b34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cparra/apexdocs", - "version": "2.0.2", + "version": "2.1.0", "description": "Library with CLI capabilities to generate documentation for Salesforce Apex classes.", "keywords": [ "apex", @@ -63,7 +63,7 @@ ] }, "dependencies": { - "@cparra/apex-reflection": "^1.0.0", + "@cparra/apex-reflection": "^1.1.1", "chalk": "^4.1.2", "html-entities": "^2.3.2", "yargs": "^16.0.3" diff --git a/src/model/markdown-file.ts b/src/model/markdown-file.ts index 9d28f387..29c33de8 100644 --- a/src/model/markdown-file.ts +++ b/src/model/markdown-file.ts @@ -55,6 +55,10 @@ export class MarkdownFile extends File { }); } + addListItem(text: string) { + this._contents += `* ${text}`; + } + protected static replaceInlineLinks(text: string) { // Parsing text to extract possible linking classes. const possibleLinks = text.match(/<<.*?>>/g); diff --git a/src/model/markdown-generation-util/field-declaration-util.ts b/src/model/markdown-generation-util/field-declaration-util.ts index 4f5d9e4e..74ca859e 100644 --- a/src/model/markdown-generation-util/field-declaration-util.ts +++ b/src/model/markdown-generation-util/field-declaration-util.ts @@ -2,12 +2,11 @@ import { MarkdownFile } from '../markdown-file'; import { FieldMirror, PropertyMirror } from '@cparra/apex-reflection'; export function declareField( - title: string, markdownFile: MarkdownFile, fields: FieldMirror[] | PropertyMirror[], startingHeadingLevel: number, + grouped = false, ) { - markdownFile.addTitle(title, startingHeadingLevel + 1); markdownFile.addBlankLine(); fields .sort((propA, propB) => { @@ -16,7 +15,7 @@ export function declareField( return 0; }) .forEach((propertyModel) => { - addFieldSection(markdownFile, propertyModel, startingHeadingLevel); + addFieldSection(markdownFile, propertyModel, startingHeadingLevel, grouped); }); markdownFile.addHorizontalRule(); @@ -24,19 +23,42 @@ export function declareField( function addFieldSection( markdownFile: MarkdownFile, - propertyModel: FieldMirror | PropertyMirror, + mirrorModel: FieldMirror | PropertyMirror, startingHeadingLevel: number, + grouped: boolean, ) { - markdownFile.addTitle(`\`${propertyModel.name}\` → \`${propertyModel.type}\``, startingHeadingLevel + 2); + if (!grouped) { + markdownFile.addTitle(`\`${mirrorModel.name}\` → \`${mirrorModel.type}\``, startingHeadingLevel + 2); + markdownFile.addBlankLine(); + + mirrorModel.annotations.forEach((annotation) => { + markdownFile.addText(`\`${annotation.type.toUpperCase()}\` `); + }); - propertyModel.annotations.forEach((annotation) => { + if (mirrorModel.docComment?.description) { + markdownFile.addBlankLine(); + markdownFile.addText(mirrorModel.docComment.description); + } markdownFile.addBlankLine(); - markdownFile.addText(`\`${annotation.type.toUpperCase()}\``); - }); + } else { + let annotations = ''; + const hasAnnotations = !!mirrorModel.annotations.length; + if (hasAnnotations) { + annotations += ' ['; + } + mirrorModel.annotations.forEach((annotation) => { + annotations += `\`${annotation.type.toUpperCase()}\` `; + }); + if (hasAnnotations) { + annotations += ']'; + } - if (propertyModel.docComment?.description) { + // If grouped we want to display these as a list + let description = ''; + if (mirrorModel.docComment?.description) { + description = ` - ${mirrorModel.docComment?.description}`; + } + markdownFile.addListItem(`\`${mirrorModel.name}\` → \`${mirrorModel.type}\`${annotations} ${description}`); markdownFile.addBlankLine(); - markdownFile.addText(propertyModel.docComment.description); } - markdownFile.addBlankLine(); } diff --git a/src/model/markdown-generation-util/method-declaration-util.ts b/src/model/markdown-generation-util/method-declaration-util.ts index 5582d08b..93cc2e6f 100644 --- a/src/model/markdown-generation-util/method-declaration-util.ts +++ b/src/model/markdown-generation-util/method-declaration-util.ts @@ -4,13 +4,11 @@ import { ParameterMirror } from '@cparra/apex-reflection/index'; import { addCustomDocCommentAnnotations } from './doc-comment-annotation-util'; export function declareMethod( - title: string, markdownFile: MarkdownFile, methods: ConstructorMirror[] | MethodMirror[], startingHeadingLevel: number, className = '', ): void { - markdownFile.addTitle(title, startingHeadingLevel + 1); methods.forEach((currentMethod) => { const signatureName = isMethod(currentMethod) ? (currentMethod as MethodMirror).name : className; markdownFile.addTitle(`\`${buildSignature(signatureName, currentMethod)}\``, startingHeadingLevel + 2); diff --git a/src/model/markdown-generation-util/type-declaration-util.ts b/src/model/markdown-generation-util/type-declaration-util.ts index 8fbbf776..31542bc1 100644 --- a/src/model/markdown-generation-util/type-declaration-util.ts +++ b/src/model/markdown-generation-util/type-declaration-util.ts @@ -2,9 +2,7 @@ import { MarkdownFile } from '../markdown-file'; import { addCustomDocCommentAnnotations } from './doc-comment-annotation-util'; import { Annotation, Type } from '@cparra/apex-reflection'; -export function declareType(markdownFile: MarkdownFile, typeMirror: Type, startingHeadingLevel: number): void { - markdownFile.addTitle(typeMirror.name, startingHeadingLevel); - +export function declareType(markdownFile: MarkdownFile, typeMirror: Type): void { typeMirror.annotations.forEach((currentAnnotation: Annotation) => { markdownFile.addBlankLine(); markdownFile.addText(`\`${currentAnnotation.type.toUpperCase()}\``); diff --git a/src/model/markdown-type-file.ts b/src/model/markdown-type-file.ts index ce73be3d..9e87e2a1 100644 --- a/src/model/markdown-type-file.ts +++ b/src/model/markdown-type-file.ts @@ -14,8 +14,16 @@ import { MarkdownFile } from './markdown-file'; import { declareType, declareMethod, declareField } from './markdown-generation-util'; import ClassFileGeneratorHelper from '../transpiler/markdown/class-file-generatorHelper'; +interface GroupAware { + group?: string; +} + +interface GroupMap { + [key: string]: GroupAware[]; +} + export class MarkdownTypeFile extends MarkdownFile implements WalkerListener { - constructor(public type: Type, public startingHeadingLevel: number = 1, headerContent?: string) { + constructor(public type: Type, public headingLevel: number = 1, headerContent?: string) { super(`${type.name}`, ClassFileGeneratorHelper.getSanitizedGroup(type)); if (headerContent) { this.addText(headerContent); @@ -29,24 +37,29 @@ export class MarkdownTypeFile extends MarkdownFile implements WalkerListener { super.addText(text, encodeHtml); } - public onTypeDeclaration(typeMirror: Type, headingLevel?: 1): void { - declareType(this, typeMirror, this.startingHeadingLevel); + public onTypeDeclaration(typeMirror: Type): void { + this.addTitle(typeMirror.name, this.headingLevel); + declareType(this, typeMirror); } public onConstructorDeclaration(className: string, constructors: ConstructorMirror[]): void { - declareMethod('Constructors', this, constructors, this.startingHeadingLevel, className); + this.addTitle('Constructors', this.headingLevel + 1); + this.declareMethodWithGroupings(constructors, className); } public onFieldsDeclaration(fields: FieldMirror[]): void { - declareField('Fields', this, fields, this.startingHeadingLevel); + this.addTitle('Fields', this.headingLevel + 1); + this.declareFieldOrProperty(fields); } public onPropertiesDeclaration(properties: PropertyMirror[]): void { - declareField('Properties', this, properties, this.startingHeadingLevel); + this.addTitle('Properties', this.headingLevel + 1); + this.declareFieldOrProperty(properties); } public onMethodsDeclaration(methods: MethodMirror[]): void { - declareMethod('Methods', this, methods, this.startingHeadingLevel); + this.addTitle('Methods', this.headingLevel + 1); + this.declareMethodWithGroupings(methods); } public onInnerEnumsDeclaration(enums: EnumMirror[]): void { @@ -62,7 +75,7 @@ export class MarkdownTypeFile extends MarkdownFile implements WalkerListener { } private addInnerTypes(title: string, types: Type[], addSeparator = true) { - this.addTitle(title, this.startingHeadingLevel + 1); + this.addTitle(title, this.headingLevel + 1); types .sort((typeA, typeB) => { if (typeA.name < typeB.name) return -1; @@ -70,11 +83,64 @@ export class MarkdownTypeFile extends MarkdownFile implements WalkerListener { return 0; }) .forEach((currentType) => { - const innerFile = new MarkdownTypeFile(currentType, this.startingHeadingLevel + 2); + const innerFile = new MarkdownTypeFile(currentType, this.headingLevel + 2); this.addText(innerFile._contents); }); if (addSeparator) { this.addHorizontalRule(); } } + + private hasGroupings(groupAware: GroupAware[]): boolean { + return !!groupAware.find((current) => !!current.group); + } + + private declareMethodWithGroupings(methods: ConstructorMirror[] | MethodMirror[], className = ''): void { + const hasGroupings = this.hasGroupings(methods); + if (!hasGroupings) { + declareMethod(this, methods, this.headingLevel, className); + } else { + const groupedConstructors = this.group(methods); + for (const key in groupedConstructors) { + this.startGroup(key); + const constructorsForGroup = groupedConstructors[key] as ConstructorMirror[]; + declareMethod(this, constructorsForGroup, this.headingLevel, className); + this.endGroup(); + } + } + } + + private declareFieldOrProperty(fieldsOrProperties: FieldMirror[] | PropertyMirror[]): void { + const hasGroupings = this.hasGroupings(fieldsOrProperties); + if (!hasGroupings) { + declareField(this, fieldsOrProperties, this.headingLevel, false); + } else { + const groupedFields = this.group(fieldsOrProperties); + for (const key in groupedFields) { + this.startGroup(key); + const fieldsForGroup = groupedFields[key] as FieldMirror[]; + declareField(this, fieldsForGroup, this.headingLevel, true); + this.endGroup(); + } + } + } + + private startGroup(groupName: string) { + this.headingLevel = this.headingLevel + 2; + this.addTitle(groupName, this.headingLevel); + } + + private endGroup() { + this.headingLevel = this.headingLevel - 2; + } + + private group(list: GroupAware[]) { + return list.reduce((groups: GroupMap, item) => { + const groupName: string = item.group ?? 'Other'; + const group: GroupAware[] = groups[groupName] || []; + group.push(item); + groups[groupName] = group; + return groups; + }, {}); + } }