diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocsPrinter.ts similarity index 64% rename from packages/docgen/src/docgen/ApiDocPrinter.ts rename to packages/docgen/src/docgen/ApiDocsPrinter.ts index cd401e41b4..d6c412d498 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocsPrinter.ts @@ -14,16 +14,8 @@ * limitations under the License. */ -import { execSync } from 'child_process'; -import MarkdownPrinter from './MarkdownPrinter'; import sortSelector from './sortSelector'; -import { ApiDoc, InterfaceInfo } from './types'; - -// TODO(Rugvip): provide through options? -const GH_BASE_URL = 'https://github.com/spotify/backstage'; - -const COMMIT_SHA = - process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8'); +import { ApiDoc, InterfaceInfo, MarkdownPrinter } from './types'; /** * The ApiDocPrinter takes a ApiDoc data structure, typically generated by an ApiDocGenerator, @@ -36,52 +28,6 @@ export default class ApiDocPrinter { this.printerFactory = printerFactory; } - mkLink( - { file, lineInFile }: { file: string; lineInFile: number }, - text?: string, - ) { - const linkText = text ?? `${file}:${lineInFile}`; - const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`; - return `[${linkText}](${href}){:target="_blank"}`; - } - - print(apiDoc: ApiDoc): Buffer { - const printer = this.printerFactory(); - - // Remove line numbers from codeblocks - printer.style('.linenodiv{ display: none }'); - - printer.header(1, apiDoc.id); - - const ifInfo = apiDoc.interfaceInfos[0]; - - if (ifInfo.docs.length) { - for (const doc of ifInfo.docs) { - printer.text(doc); - } - } else { - printer.paragraph(apiDoc.description); - } - - printer.header(2, 'API Interface'); - printer.paragraph( - `The API interface type is defined at ${this.mkLink(ifInfo)} as ${ - ifInfo.name - }.`, - ); - printer.paragraph('All members of the interface are listed below.'); - - this.addInterfaceMembers(printer, ifInfo); - - if (ifInfo.dependentTypes.length) { - printer.header(2, 'Types'); - - this.addInterfaceTypes(printer, ifInfo); - } - - return printer.toBuffer(); - } - printApiIndex(apiDocs: ApiDoc[]): Buffer { const printer = this.printerFactory(); @@ -89,8 +35,8 @@ export default class ApiDocPrinter { printer.paragraph( 'The following is a list of all Utility APIs defined by `@backstage/core`.', - 'They are available to use by plugins and components, and need to be provided by the app', - 'They can be accessed using the `useApi` hook, also provided by `@backstage/core`.', + 'They are available to use by plugins and components, and can be accessed ', + 'using the `useApi` hook, also provided by `@backstage/core`.', 'For more information, see https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md.', ); @@ -99,14 +45,14 @@ export default class ApiDocPrinter { printer.paragraph(api.description); - const typeLinks = api.interfaceInfos.map(i => `[${i.name}](${i.name})`); + const typeLinks = api.interfaceInfos.map(i => `[${i.name}](./${i.name})`); printer.paragraph( `Implemented type${typeLinks.length > 1 ? 's' : ''}: ${typeLinks.join( ', ', )}`, ); - printer.paragraph(`ApiRef: ${this.mkLink(api, api.name)}`); + printer.paragraph(`ApiRef: ${printer.srcLink(api, api.name)}`); } return printer.toBuffer(); @@ -115,18 +61,18 @@ export default class ApiDocPrinter { printInterface(apiType: InterfaceInfo, apiDocs: ApiDoc[]): Buffer { const printer = this.printerFactory(); - // Remove line numbers from codeblocks - printer.style('.linenodiv{ display: none }'); - printer.header(1, apiType.name); printer.paragraph( - `The ${apiType.name} type is defined at ${this.mkLink(apiType)}.`, + `The ${apiType.name} type is defined at ${printer.srcLink(apiType)}.`, ); const apiLinks = apiDocs .filter(ad => ad.interfaceInfos.some(i => i.name === apiType.name)) - .map(ad => `[${ad.name}](../#${ad.id})`); + .map(ad => { + const link = printer.indexLink() + printer.headerLink(ad.name, ad.id); + return `[${ad.name}](${link})`; + }); if (apiLinks.length === 1) { printer.paragraph( @@ -170,7 +116,7 @@ export default class ApiDocPrinter { printer.text(doc); } - printer.codeWithLinks(member); + printer.code(member); } } @@ -183,15 +129,17 @@ export default class ApiDocPrinter { for (const doc of type.docs) { printer.text(doc); } - printer.codeWithLinks(type); + printer.code(type); - printer.paragraph(`Defined at ${this.mkLink(type)}.`); + printer.paragraph(`Defined at ${printer.srcLink(type)}.`); const usageLinks = [...apiType.members, ...apiType.dependentTypes] .filter(member => { return member.links.some(link => link.id === type.id); }) - .map(({ name, path }) => `[${name}](#${path})`); + .map( + ({ name, path }) => `[${name}](${printer.headerLink(name, path)})`, + ); if (usageLinks.length) { printer.paragraph(`Referenced by: ${usageLinks.join(', ')}.`); diff --git a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts new file mode 100644 index 0000000000..391f50a01d --- /dev/null +++ b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import sortSelector from './sortSelector'; +import { MarkdownPrinter, TypeLink } from './types'; +import { execSync } from 'child_process'; + +// TODO(Rugvip): provide through options? +const GH_BASE_URL = 'https://github.com/spotify/backstage'; + +const COMMIT_SHA = + process.env.COMMIT_SHA || + execSync('git rev-parse HEAD').toString('utf8').trim(); + +/** + * The MarkdownPrinter is a helper class for building markdown documents. + */ +export default class GithubMarkdownPrinter implements MarkdownPrinter { + private str: string = ''; + + private line(line: string = '') { + this.str += `${line}\n`; + } + + text(text: string) { + this.line(text); + this.line(); + } + + header(level: number, text: string) { + this.line(`${'#'.repeat(level)} ${text}`); + this.line(); + } + + headerLink(heading: string): string { + const slug = heading.replace(/[^a-z0-9]/gi, '-').toLowerCase(); + return `#${slug}`; + } + + indexLink() { + return './README.md'; + } + + srcLink( + { file, lineInFile }: { file: string; lineInFile: number }, + text?: string, + ): string { + const linkText = text ?? `${file}:${lineInFile}`; + const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`; + return `[${linkText}](${href})`; + } + + paragraph(...text: string[]) { + this.line( + text + .join('\n') + .trim() + .split('\n') + .map(line => line.trim()) + .join('\n'), + ); + this.line(); + } + + code({ text, links = [] }: { text: string; links?: TypeLink[] }) { + this.line('
');
+ this.line(this.formatWithLinks({ text, links }));
+ this.line('');
+ this.line();
+ }
+
+ private escapeText: (text: string) => string = (() => {
+ const escapes: { [char in string]: string } = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ };
+
+ return (text: string) => text.replace(/[&<>]/g, char => escapes[char]);
+ })();
+
+ private formatWithLinks({
+ text,
+ links = [],
+ }: {
+ text: string;
+ links?: TypeLink[];
+ }): string {
+ const sortedLinks = links.slice().sort(sortSelector(x => x.location[0]));
+
+ sortedLinks.reduce((lastEnd, link) => {
+ if (link.location[0] <= lastEnd) {
+ throw new Error(
+ `overlapping link detected for ${link.path}, ${link.location}`,
+ );
+ }
+ return link.location[1];
+ }, -1);
+
+ const parts: Array<{ text: string; link?: boolean }> = [];
+
+ const endLocation = sortedLinks.reduce((prev, link) => {
+ const [start, end] = link.location;
+ parts.push(
+ { text: text.slice(prev, start) },
+ { text: text.slice(start, end), link: true },
+ );
+ return end;
+ }, 0);
+
+ parts.push({ text: text.slice(endLocation) });
+
+ return parts
+ .map(part => {
+ if (part.link) {
+ const link = this.headerLink(part.text);
+ return `${this.escapeText(part.text)}`;
+ }
+ return this.escapeText(part.text);
+ })
+ .join('');
+ }
+
+ toBuffer(): Buffer {
+ return Buffer.from(this.str, 'utf8');
+ }
+}
diff --git a/packages/docgen/src/docgen/MarkdownPrinter.ts b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts
similarity index 74%
rename from packages/docgen/src/docgen/MarkdownPrinter.ts
rename to packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts
index fb64863da9..3609e0526d 100644
--- a/packages/docgen/src/docgen/MarkdownPrinter.ts
+++ b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts
@@ -15,17 +15,27 @@
*/
import sortSelector from './sortSelector';
-import { Highlighter, TypeLink } from './types';
+import { Highlighter, MarkdownPrinter, TypeLink } from './types';
+import { execSync } from 'child_process';
+
+// TODO(Rugvip): provide through options?
+const GH_BASE_URL = 'https://github.com/spotify/backstage';
+
+const COMMIT_SHA =
+ process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8');
/**
* The MarkdownPrinter is a helper class for building markdown documents.
*/
-export default class MarkdownPrinter {
+export default class TechdocsMarkdownPrinter implements MarkdownPrinter {
private str: string = '';
private readonly highlighter: Highlighter;
constructor(highlighter: Highlighter) {
this.highlighter = highlighter;
+
+ // Remove line numbers from codeblocks
+ this.style('.linenodiv{ display: none }');
}
private line(line: string = '') {
@@ -48,6 +58,23 @@ export default class MarkdownPrinter {
this.line();
}
+ headerLink(heading: string, id?: string): string {
+ return `#${id ?? heading}`;
+ }
+
+ indexLink() {
+ return '../';
+ }
+
+ srcLink(
+ { file, lineInFile }: { file: string; lineInFile: number },
+ text?: string,
+ ): string {
+ const linkText = text ?? `${file}:${lineInFile}`;
+ const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`;
+ return `[${linkText}](${href})`;
+ }
+
paragraph(...text: string[]) {
this.line(
text
@@ -60,14 +87,7 @@ export default class MarkdownPrinter {
this.line();
}
- code(syntax: string = '', block: string) {
- this.line(`\`\`\`${syntax}`);
- this.line(block);
- this.line('```');
- this.line();
- }
-
- codeWithLinks({ text, links = [] }: { text: string; links?: TypeLink[] }) {
+ code({ text, links = [] }: { text: string; links?: TypeLink[] }) {
this.line('