Skip to content

Commit 0c5ef70

Browse files
fix: claude code's first pass at generating markdown
1 parent 549479e commit 0c5ef70

31 files changed

Lines changed: 1411 additions & 108 deletions

messages/main.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ fail the command if there are any warnings
2828

2929
# flags.ditamap-suffix.summary
3030

31-
unique suffix to append to generated ditamap
31+
unique suffix to append to generated DITA files
32+
33+
# flags.output-format.summary
34+
35+
output format for generated documentation; 'dita' (default) generates DITA XML files, 'markdown' generates Markdown files
3236

3337
# flags.config-path.summary
3438

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,17 @@
196196
"messages/**/*.md"
197197
],
198198
"output": []
199+
},
200+
"test:command-reference-markdown": {
201+
"command": "node --loader ts-node/esm --no-warnings=ExperimentalWarning \"./bin/dev.js\" commandreference generate --plugins auth --plugins user --output-format markdown --outputdir test/tmp-md",
202+
"files": [
203+
"src/**/*.ts",
204+
"messages/**",
205+
"package.json"
206+
],
207+
"output": [
208+
"test/tmp-md"
209+
]
199210
}
200211
}
201212
}

src/commands/commandreference/generate.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ export default class CommandReferenceGenerate extends SfCommand<CommandReference
7373
summary: messages.getMessage('flags.config-path.summary'),
7474
char: 'c',
7575
}),
76+
'output-format': Flags.string({
77+
summary: messages.getMessage('flags.output-format.summary'),
78+
options: ['dita', 'markdown'],
79+
default: 'dita',
80+
}),
7681
};
7782

7883
private loadedConfig!: Interfaces.Config;
@@ -146,9 +151,13 @@ export default class CommandReferenceGenerate extends SfCommand<CommandReference
146151
const commands = await this.loadCommands(plugins);
147152
const topicMetadata = this.loadTopicMetadata(commands);
148153
const cliMeta = this.loadCliMeta();
149-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
150-
// @ts-ignore
151-
const docs = new Docs(Ditamap.outputDir, flags.hidden, topicMetadata, cliMeta);
154+
const docs = new Docs(
155+
Ditamap.outputDir,
156+
flags['output-format'] as 'dita' | 'markdown',
157+
flags.hidden,
158+
topicMetadata ?? new Map<string, never>(),
159+
cliMeta
160+
);
152161

153162
events.on('topic', ({ topic }: { topic: string }) => {
154163
this.log(chalk.green(`Generating topic '${topic}'`));

src/ditamap/command-helpers.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Dictionary, Optional } from '@salesforce/ts-types';
18+
import { CommandParameterData, replaceConfigVariables } from '../utils.js';
19+
20+
export type FlagInfo = {
21+
hidden: boolean;
22+
description: string;
23+
summary: string;
24+
required: boolean;
25+
kind: string;
26+
type: string;
27+
defaultHelpValue?: string;
28+
default: string | (() => Promise<string>);
29+
aliases?: string[];
30+
options?: string[];
31+
char?: string;
32+
deprecated?: { version: string; to: string };
33+
};
34+
35+
export const getDefault = async (flag: FlagInfo, flagName: string): Promise<string> => {
36+
if (!flag) {
37+
return '';
38+
}
39+
if (flagName === 'target-org' || flagName === 'target-dev-hub') {
40+
return '';
41+
}
42+
if (typeof flag.default === 'function') {
43+
try {
44+
const help = await flag.default();
45+
return help.includes('[object Object]') ? '' : help ?? '';
46+
} catch {
47+
return '';
48+
}
49+
} else {
50+
return flag.default;
51+
}
52+
};
53+
54+
export const flagIsDefined = (input: [string, Optional<FlagInfo>]): input is [string, FlagInfo] =>
55+
input[1] !== undefined;
56+
57+
export const buildDescription =
58+
(commandName: string) =>
59+
(binary: string) =>
60+
(flag: FlagInfo): string[] => {
61+
const description = replaceConfigVariables(
62+
Array.isArray(flag?.description) ? flag?.description.join('\n') : flag?.description ?? '',
63+
binary,
64+
commandName
65+
);
66+
return formatParagraphs(
67+
flag.summary ? `${replaceConfigVariables(flag.summary, binary, commandName)}\n${description}` : description
68+
);
69+
};
70+
71+
export const formatParagraphs = (textToFormat?: string): string[] =>
72+
textToFormat ? textToFormat.split('\n').filter((n) => n !== '') : [];
73+
74+
export const readBinary = (commandMeta: Record<string, unknown>): string =>
75+
'binary' in commandMeta && typeof commandMeta.binary === 'string' ? commandMeta.binary : 'unknown';
76+
77+
export const buildCommandParameters = async (
78+
commandName: string,
79+
binary: string,
80+
flags: Dictionary<FlagInfo>
81+
): Promise<CommandParameterData[]> => {
82+
const descriptionBuilder = buildDescription(commandName)(binary);
83+
return Promise.all(
84+
[...Object.entries(flags)]
85+
.filter(flagIsDefined)
86+
.filter(([, flag]) => !flag.hidden)
87+
.map(
88+
async ([flagName, flag]) =>
89+
({
90+
...flag,
91+
name: flagName,
92+
description: descriptionBuilder(flag),
93+
optional: !flag.required,
94+
kind: flag.kind ?? flag.type,
95+
hasValue: flag.type !== 'boolean',
96+
defaultFlagValue: await getDefault(flag, flagName),
97+
} satisfies CommandParameterData)
98+
)
99+
);
100+
};

src/ditamap/command.ts

Lines changed: 4 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,10 @@
1515
*/
1616

1717
import { join } from 'node:path';
18-
import { asString, Dictionary, ensureObject, ensureString, Optional } from '@salesforce/ts-types';
19-
import { CommandClass, CommandData, CommandParameterData, punctuate, replaceConfigVariables } from '../utils.js';
18+
import { asString, Dictionary, ensureObject, ensureString } from '@salesforce/ts-types';
19+
import { CommandClass, CommandData, punctuate, replaceConfigVariables } from '../utils.js';
2020
import { Ditamap } from './ditamap.js';
21-
22-
type FlagInfo = {
23-
hidden: boolean;
24-
description: string;
25-
summary: string;
26-
required: boolean;
27-
kind: string;
28-
type: string;
29-
defaultHelpValue?: string;
30-
default: string | (() => Promise<string>);
31-
};
32-
33-
const getDefault = async (flag: FlagInfo, flagName: string): Promise<string> => {
34-
if (!flag) {
35-
return '';
36-
}
37-
if (flagName === 'target-org' || flagName === 'target-dev-hub') {
38-
// special handling to prevent global/local default usernames from appearing in the docs, but they do appear in user's help
39-
return '';
40-
}
41-
if (typeof flag.default === 'function') {
42-
try {
43-
const help = await flag.default();
44-
return help.includes('[object Object]') ? '' : help ?? '';
45-
} catch {
46-
return '';
47-
}
48-
} else {
49-
return flag.default;
50-
}
51-
};
21+
import { buildCommandParameters, FlagInfo, readBinary, formatParagraphs } from './command-helpers.js';
5222

5323
export class Command extends Ditamap {
5424
private flags: Dictionary<FlagInfo>;
@@ -131,57 +101,14 @@ export class Command extends Ditamap {
131101
this.destination = join(Ditamap.outputDir, topic, filename);
132102
}
133103

134-
public async getParametersForTemplate(flags: Dictionary<FlagInfo>): Promise<CommandParameterData[]> {
135-
const descriptionBuilder = buildDescription(this.commandName)(readBinary(this.commandMeta));
136-
return Promise.all(
137-
[...Object.entries(flags)]
138-
.filter(flagIsDefined)
139-
.filter(([, flag]) => !flag.hidden)
140-
.map(
141-
async ([flagName, flag]) =>
142-
({
143-
...flag,
144-
name: flagName,
145-
description: descriptionBuilder(flag),
146-
optional: !flag.required,
147-
kind: flag.kind ?? flag.type,
148-
hasValue: flag.type !== 'boolean',
149-
defaultFlagValue: await getDefault(flag, flagName),
150-
} satisfies CommandParameterData)
151-
)
152-
);
153-
}
154-
155104
// eslint-disable-next-line class-methods-use-this
156105
public getTemplateFileName(): string {
157106
return 'command.hbs';
158107
}
159108

160109
protected async transformToDitamap(): Promise<string> {
161-
const parameters = await this.getParametersForTemplate(this.flags);
110+
const parameters = await buildCommandParameters(this.commandName, readBinary(this.commandMeta), this.flags);
162111
this.data = Object.assign({}, this.data, { parameters });
163112
return super.transformToDitamap();
164113
}
165114
}
166-
167-
const flagIsDefined = (input: [string, Optional<FlagInfo>]): input is [string, FlagInfo] => input[1] !== undefined;
168-
169-
const buildDescription =
170-
(commandName: string) =>
171-
(binary: string) =>
172-
(flag: FlagInfo): string[] => {
173-
const description = replaceConfigVariables(
174-
Array.isArray(flag?.description) ? flag?.description.join('\n') : flag?.description ?? '',
175-
binary,
176-
commandName
177-
);
178-
return formatParagraphs(
179-
flag.summary ? `${replaceConfigVariables(flag.summary, binary, commandName)}\n${description}` : description
180-
);
181-
};
182-
183-
const formatParagraphs = (textToFormat?: string): string[] =>
184-
textToFormat ? textToFormat.split('\n').filter((n) => n !== '') : [];
185-
186-
const readBinary = (commandMeta: Record<string, unknown>): string =>
187-
'binary' in commandMeta && typeof commandMeta.binary === 'string' ? commandMeta.binary : 'unknown';

src/docs.ts

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,8 @@
1717
import fs from 'node:fs/promises';
1818
import { AnyJson, ensureString } from '@salesforce/ts-types';
1919
import chalk from 'chalk';
20-
import { BaseDitamap } from './ditamap/base-ditamap.js';
21-
import { CLIReference } from './ditamap/cli-reference.js';
22-
import { Command } from './ditamap/command.js';
23-
import { TopicCommands } from './ditamap/topic-commands.js';
24-
import { TopicDitamap } from './ditamap/topic-ditamap.js';
2520
import { CliMeta, events, punctuate, SfTopic, SfTopics, CommandClass } from './utils.js';
26-
import { HelpReference } from './ditamap/help-reference.js';
21+
import { DitaGeneratorFactory, GeneratorFactory, MarkdownGeneratorFactory, OutputFormat } from './generator-factory.js';
2722

2823
type TopicsByTopicsByTopLevel = Map<string, Map<string, CommandClass[]>>;
2924

@@ -37,12 +32,17 @@ function emitNoTopicMetadataWarning(topic: string): void {
3732
}
3833

3934
export class Docs {
35+
private factory: GeneratorFactory;
36+
4037
public constructor(
4138
private outputDir: string,
39+
outputFormat: OutputFormat,
4240
private hidden: boolean,
4341
private topicMeta: SfTopics,
4442
private cliMeta: CliMeta
45-
) {}
43+
) {
44+
this.factory = outputFormat === 'markdown' ? new MarkdownGeneratorFactory(outputDir) : new DitaGeneratorFactory();
45+
}
4646

4747
public async build(commands: CommandClass[]): Promise<void> {
4848
// Create if doesn't exist
@@ -77,13 +77,6 @@ export class Docs {
7777

7878
for (const [subtopic, classes] of subtopics.entries()) {
7979
try {
80-
// const subTopicsMeta = topicMeta.subtopics;
81-
82-
// if (!subTopicsMeta?.get(subtopic)) {
83-
// emitNoTopicMetadataWarning(`${topic}:${subtopic}`);
84-
// continue;
85-
// }
86-
8780
subTopicNames.push(subtopic);
8881

8982
// Commands within the sub topic
@@ -110,8 +103,8 @@ export class Docs {
110103
// The topic ditamap with all of the subtopic links.
111104
events.emit('subtopics', topic, subTopicNames);
112105

113-
await new TopicCommands(topic, topicMeta).write();
114-
await new TopicDitamap(topic, commandIds).write();
106+
await this.factory.createTopicCommands(topic, topicMeta).write();
107+
await this.factory.createTopicIndex(topic, commandIds).write();
115108
return subTopicNames;
116109
}
117110

@@ -123,7 +116,6 @@ export class Docs {
123116
* @returns The commands grouped by topics/subtopic/commands.
124117
*/
125118
private groupTopicsAndSubtopics(commands: CommandClass[]): TopicsByTopicsByTopLevel {
126-
// const topLevelTopics: Dictionary<Dictionary<CommandClass | CommandClass[]>> = {};
127119
const topLevelTopics = new Map<string, Map<string, CommandClass[]>>();
128120

129121
for (const command of commands) {
@@ -135,17 +127,14 @@ export class Docs {
135127

136128
const plugin = command.plugin;
137129
if (plugin) {
138-
// Also include the namespace on the commands so we don't need to do the split at other times in the code.
139130
command.topic = topLevelTopic;
140131

141132
const existingTopicsForTopLevel = topLevelTopics.get(topLevelTopic) ?? new Map<string, CommandClass[]>();
142133

143134
if (commandParts.length === 1) {
144-
// This is a top-level topic that is also a command
145135
const existingTarget = existingTopicsForTopLevel.get(commandParts[0]) ?? [];
146136
existingTopicsForTopLevel.set(commandParts[0], [...existingTarget, command]);
147137
} else if (commandParts.length === 2) {
148-
// This is a command directly under the top-level topic
149138
const existingTarget = existingTopicsForTopLevel.get(commandParts[1]) ?? [];
150139
existingTopicsForTopLevel.set(commandParts[1], [...existingTarget, command]);
151140
} else {
@@ -177,11 +166,12 @@ export class Docs {
177166
private async populateTemplate(commands: CommandClass[]): Promise<void> {
178167
const topicsAndSubtopics = this.groupTopicsAndSubtopics(commands);
179168

180-
await new CLIReference().write();
181-
await new HelpReference().write();
169+
await this.factory.createCliReference().write();
170+
171+
const helpReference = this.factory.createHelpReference();
172+
if (helpReference) await helpReference.write();
182173

183-
// Generate one base file with all top-level topics.
184-
await new BaseDitamap(Array.from(topicsAndSubtopics.keys())).write();
174+
await this.factory.createRootIndex(Array.from(topicsAndSubtopics.keys())).write();
185175

186176
for (const [topic, subtopics] of topicsAndSubtopics.entries()) {
187177
events.emit('topic', { topic });
@@ -240,8 +230,8 @@ export class Docs {
240230
return '';
241231
}
242232

243-
const commandDitamap = new Command(topic, subtopic, command, commandMeta);
244-
await commandDitamap.write();
245-
return commandDitamap.getFilename();
233+
const commandGenerator = this.factory.createCommand(topic, subtopic, command, commandMeta);
234+
await commandGenerator.write();
235+
return commandGenerator.getFilename();
246236
}
247237
}

0 commit comments

Comments
 (0)