Skip to content

Commit

Permalink
feat: link to source code (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterpeterparker committed Jul 2, 2023
1 parent 9f28b9b commit 18a36bf
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 35 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ The output file will be over write unless you specify a `TSDOC_START` and `TSDOC

Specifying another output file is also supported with the parameter `--dest`.

To generate links to the documented source code, you can also provide the `--repo` parameter, which corresponds to the URL of your repository on GitHub.

Using above script is of course optional. You can also develop your own JavaScript script and use one of the functions here under.

e.g.
Expand Down Expand Up @@ -98,6 +100,8 @@ Parameters:
- `params.inputFiles`: The list of files to scan and for which the documentation should be build.
- `params.options`: Optional compiler options to generate the docs

[:link: Source](https://github.com/peterpeterparker/tsdoc-markdown/tree/main/src/lib/docs.ts#L212)

### :gear: documentationToMarkdown

Convert the documentation entries to an opinionated Markdown format.
Expand All @@ -111,6 +115,8 @@ Parameters:
- `params.entries`: The entries of the documentation (functions, constants and classes).
- `params.options`: Optional configuration to render the Markdown content. See `types.ts` for details.

[:link: Source](https://github.com/peterpeterparker/tsdoc-markdown/tree/main//home/runner/work/tsdoc-markdown/tsdoc-markdown/src/lib/markdown.ts#L221)

### :gear: generateDocumentation

Generate documentation and write output to a file.
Expand All @@ -128,6 +134,8 @@ Parameters:
- `params.markdownOptions`: Optional settings passed to the Markdown parser. See `MarkdownOptions` for details.
- `params.buildOptions`: Options to construct the documentation tree. See `BuildOptions` for details.

[:link: Source](https://github.com/peterpeterparker/tsdoc-markdown/tree/main/src/lib/index.ts#L27)

<!-- TSDOC_END -->

## Useful Resources
Expand Down
15 changes: 14 additions & 1 deletion bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ if (help !== undefined) {

console.log('\nOptions:');
console.log('--dest=<destination file> (default README.md)');
console.log('--repo=<GitHub repo URL>');
return;
}

Expand All @@ -34,8 +35,20 @@ const inputFiles = listInputs();
const outputFile =
process.argv.find((arg) => arg.indexOf('--dest=') > -1)?.replace('--dest=', '') ?? 'README.md';

const repoUrl = process.argv.find((arg) => arg.indexOf('--repo=') > -1)?.replace('--repo=', '');

if (!inputFiles || inputFiles.length === 0) {
throw new Error('No source file(s) provided.');
}

generateDocumentation({inputFiles, outputFile});
generateDocumentation({
inputFiles,
outputFile,
...(repoUrl !== undefined && {
markdownOptions: {
repo: {
url: repoUrl
}
}
})
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"build": "node rmdir.mjs && node esbuild.mjs && npm run ts-declaration",
"ts-declaration": "tsc --emitDeclarationOnly --outDir dist/types",
"test": "jest",
"start": "node rmdir.mjs && node esbuild.mjs && node bin/index.js --src=src/test/mock.ts --dest=src/test/mock.md",
"docs": "node rmdir.mjs && node esbuild.mjs && node bin/index.js --src=src/lib/* && prettier --write ./README.md"
"start": "node rmdir.mjs && node esbuild.mjs && node bin/index.js --src=src/test/mock.ts --dest=src/test/mock.md --repo=https://github.com/peterpeterparker/tsdoc-markdown",
"docs": "node rmdir.mjs && node esbuild.mjs && node bin/index.js --src=src/lib/* --repo=https://github.com/peterpeterparker/tsdoc-markdown && prettier --write ./README.md"
},
"repository": {
"type": "git",
Expand Down
9 changes: 8 additions & 1 deletion src/lib/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,14 @@ export const buildDocumentation = ({
// Walk the tree to search for classes
forEachChild(sourceFile, (node: Node) => {
const entries: DocEntry[] = visit({checker, node});
result.push(...entries.map((entry: DocEntry) => ({...entry, fileName: sourceFile.fileName})));
const {line} = sourceFile.getLineAndCharacterOfPosition(node.getStart());
result.push(
...entries.map((entry: DocEntry) => ({
...entry,
line: line + 1,
fileName: sourceFile.fileName
}))
);
});
}

Expand Down
103 changes: 74 additions & 29 deletions src/lib/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import type {

type Params = {name: string; documentation: string};

type Row = Required<Pick<DocEntry, 'name' | 'type' | 'documentation'>> & {
params: Params[];
};
type Row = Required<Pick<DocEntry, 'name' | 'type' | 'documentation'>> &
Pick<DocEntry, 'line' | 'fileName'> & {
params: Params[];
};

const toParams = (parameters?: DocEntry[]): Params[] =>
(parameters ?? []).map(({name, documentation}: DocEntry) => ({
Expand All @@ -28,16 +29,16 @@ const inlineParams = (params: Params[]): string[] =>
const classesToMarkdown = ({
entry,
headingLevel,
emoji
emoji,
repo
}: {
entry: DocEntry;
headingLevel: MarkdownHeadingLevel;
emoji: MarkdownEmoji | undefined;
}): string => {
const {name, documentation, methods, constructors} = entry;
} & Required<Pick<MarkdownOptions, 'headingLevel'>> &
Omit<MarkdownOptions, 'headingLevel'>): string => {
const {name, fileName, line, documentation, methods, constructors} = entry;

const markdown: string[] = [`${headingLevel}${emojiTitle({emoji, key: 'classes'})} ${name}\n`];
(documentation !== undefined && documentation !== '') && markdown.push(`${documentation}\n`);
documentation !== undefined && documentation !== '' && markdown.push(`${documentation}\n`);

const publicConstructors: DocEntryConstructor[] = (constructors ?? []).filter(
({visibility}) => visibility === 'public'
Expand Down Expand Up @@ -69,22 +70,46 @@ const classesToMarkdown = ({
markdown.push(`${headingLevel}# Methods\n`);
markdown.push(`${tableOfContent({entries: methods ?? [], emoji})}\n`);

// Explicitly do not pass repo to generate the source code link afterwards for the all block
markdown.push(
`${toMarkdown({entries: methods ?? [], headingLevel: `${headingLevel}#`, docType: 'Method'})}\n`
`${toMarkdown({
entries: methods ?? [],
headingLevel: `${headingLevel}#`,
docType: 'Method',
})}\n`
);

if (repo !== undefined) {
markdown.push(sourceCodeLink({repo, emoji, fileName, line}));
}

return markdown.join('\n');
};

const sourceCodeLink = ({
repo,
emoji,
fileName,
line
}: Pick<MarkdownOptions, 'emoji'> &
Required<Pick<MarkdownOptions, 'repo'>> &
Pick<DocEntry, 'line' | 'fileName'>): string => {
const {url, branch} = repo;
const sourceCodeUrl = `${url.replace(/\/+$/, '')}/tree/${branch ?? 'main'}/${fileName}#L${line}`;
return `[${emojiTitle({emoji, key: 'link'}).trim()} Source](${sourceCodeUrl})\n`;
};

const toMarkdown = ({
entries,
headingLevel,
docType
docType,
emoji,
repo
}: {
entries: DocEntry[];
headingLevel: MarkdownHeadingLevel | '####';
docType: 'Constant' | 'Function' | 'Method';
}): string => {
} & Pick<MarkdownOptions, 'emoji' | 'repo'>): string => {
const jsDocsToParams = (jsDocs: JSDocTagInfo[]): Params[] => {
const params: JSDocTagInfo[] = jsDocs.filter(({name}: JSDocTagInfo) => name === 'param');
const texts: (SymbolDisplayPart[] | undefined)[] = params.map(({text}) => text);
Expand Down Expand Up @@ -114,17 +139,21 @@ const toMarkdown = ({
return parts.map(toParam).filter((param) => param !== undefined) as Params[];
};

const rows: Row[] = entries.map(({name, type, documentation, parameters, jsDocs}: DocEntry) => ({
name,
type: type ?? '',
documentation: documentation ?? '',
params: [...toParams(parameters), ...jsDocsToParams(jsDocs ?? [])]
}));
const rows: Row[] = entries.map(
({name, type, documentation, parameters, jsDocs, line, fileName}: DocEntry) => ({
name,
type: type ?? '',
documentation: documentation ?? '',
params: [...toParams(parameters), ...jsDocsToParams(jsDocs ?? [])],
line,
fileName
})
);

// Avoid issue if the Markdown table gets formatted with Prettier
const parseType = (type: string): string => type.replace(/ \| /, ' or ').replace(/ & /, ' and ');

const rowToMarkdown = ({name, documentation, type, params}: Row): string => {
const rowToMarkdown = ({name, documentation, type, params, line, fileName}: Row): string => {
const markdown: string[] = [`${headingLevel}# :gear: ${name}\n`];

if (documentation.length) {
Expand All @@ -141,6 +170,10 @@ const toMarkdown = ({
markdown.push('\n');
}

if (repo !== undefined) {
markdown.push(sourceCodeLink({repo, emoji, fileName, line}));
}

return markdown.join('\n');
};

Expand All @@ -152,12 +185,11 @@ const tableOfContent = ({
emoji
}: {
entries: DocEntry[];
emoji: MarkdownEmoji | undefined;
}): string =>
} & Pick<MarkdownOptions, 'emoji'>): string =>
entries
.map(
({name}) =>
`- [${name}](#${emoji === undefined ? '' : `${emoji.entry}-`}${name
`- [${name}](#${emoji === undefined || emoji === null ? '' : `${emoji.entry}-`}${name
.toLowerCase()
.replace(/ /g, '-')})`
)
Expand All @@ -167,15 +199,16 @@ const emojiTitle = ({
emoji,
key
}: {
emoji: MarkdownEmoji | undefined;
key: keyof MarkdownEmoji;
}): string => (emoji === undefined ? '' : ` :${emoji[key]}:`);
} & Pick<MarkdownOptions, 'emoji'>): string =>
emoji === undefined || emoji === null ? '' : ` :${emoji[key]}:`;

const DEFAULT_EMOJI: MarkdownEmoji = {
classes: 'factory',
functions: 'toolbox',
constants: 'wrench',
entry: 'gear'
entry: 'gear',
link: 'link'
};

/**
Expand All @@ -192,7 +225,13 @@ export const documentationToMarkdown = ({
entries: DocEntry[];
options?: MarkdownOptions;
}): string => {
const {headingLevel, emoji: userEmoji} = options ?? {headingLevel: '##', emoji: DEFAULT_EMOJI};
const {
headingLevel: userHeadingLevel,
emoji: userEmoji,
repo
} = options ?? {headingLevel: '##', emoji: DEFAULT_EMOJI};

const headingLevel = userHeadingLevel ?? '##';

const emoji: MarkdownEmoji | undefined =
userEmoji === null ? undefined : userEmoji ?? DEFAULT_EMOJI;
Expand All @@ -206,17 +245,23 @@ export const documentationToMarkdown = ({
if (functions.length) {
markdown.push(`${headingLevel}${emojiTitle({emoji, key: 'functions'})} Functions\n`);
markdown.push(`${tableOfContent({entries: functions, emoji})}\n`);
markdown.push(`${toMarkdown({entries: functions, headingLevel, docType: 'Function'})}\n`);
markdown.push(
`${toMarkdown({entries: functions, headingLevel, emoji, repo, docType: 'Function'})}\n`
);
}

if (constants.length) {
markdown.push(`${headingLevel}${emojiTitle({emoji, key: 'constants'})} Constants\n`);
markdown.push(`${tableOfContent({entries: constants, emoji})}\n`);
markdown.push(`${toMarkdown({entries: constants, headingLevel, docType: 'Constant'})}\n`);
markdown.push(
`${toMarkdown({entries: constants, headingLevel, emoji, repo, docType: 'Constant'})}\n`
);
}

markdown.push(
classes.map((entry: DocEntry) => classesToMarkdown({entry, headingLevel, emoji})).join('\n')
classes
.map((entry: DocEntry) => classesToMarkdown({entry, headingLevel, emoji, repo}))
.join('\n')
);

return markdown.join('\n');
Expand Down
12 changes: 11 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ export interface DocEntry {
returnType?: string;
jsDocs?: JSDocTagInfo[];
doc_type?: DocEntryType;
line?: number;
}

export interface MarkdownRepo {
url: string;
// Default: "main" branch
branch?: string;
}

/**
Expand All @@ -29,6 +36,7 @@ export interface MarkdownEmoji {
constants: string;
// A function, method or constant title - i.e. an entry of one above titles
entry: string;
link: string;
}

export type MarkdownHeadingLevel = '#' | '##' | '###';
Expand All @@ -40,7 +48,9 @@ export interface MarkdownOptions {
// Emoji configuration. `undefined` for default configuration, `null` for explicitly no emoji.
emoji?: MarkdownEmoji | null;
// The base heading level at which the documentation should start. Default ##
headingLevel: MarkdownHeadingLevel;
headingLevel?: MarkdownHeadingLevel;
// If provided, the Markdown parser will generate links to the documented source code
repo?: MarkdownRepo;
}

/**
Expand Down
29 changes: 28 additions & 1 deletion src/test/markdown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,36 @@ describe('markdown', () => {
inputFiles: ['./src/test/mock.ts']
});

const markdown: string = documentationToMarkdown({entries: doc});
const markdown: string = documentationToMarkdown({
entries: doc,
options: {
repo: {
url: 'https://github.com/peterpeterparker/tsdoc-markdown'
}
}
});

const expectedDoc = readFileSync('./src/test/mock.md', 'utf8');

expect(markdown).toEqual(expectedDoc);
});

it.each([35, 86, 114])('should generate a markdown link to line %s', (line) => {
const doc = buildDocumentation({
inputFiles: ['./src/test/mock.ts']
});

const markdown: string = documentationToMarkdown({
entries: doc,
options: {
repo: {
url: 'https://github.com/peterpeterparker/tsdoc-markdown/'
}
}
});

expect(markdown).toContain(
`[:link: Source](https://github.com/peterpeterparker/tsdoc-markdown/tree/main/src/test/mock.ts#L${line})`
);
});
});
Loading

0 comments on commit 18a36bf

Please sign in to comment.