Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚙️Fix broken anchor issue in api doc generation #275

Merged
merged 8 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
9 changes: 8 additions & 1 deletion .github/cicd/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"deno.enable": true,
"files.eol": "\n",
"files.exclude": {
"**/RepoSrc": true
},
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno",
}
"editor.insertSpaces": false,
"editor.tabSize": 3,
},
}
74 changes: 66 additions & 8 deletions .github/cicd/core/services/MarkdownFileContentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { MarkdownHeaderService } from "./MarkdownHeaderService.ts";
import { MarkdownService } from "./MarkdownService.ts";
import { Utils } from "../Utils.ts";
import { basename, extname } from "../../deps.ts";
import { ProcessFragmentService } from "./ProcessFragmentService.ts";

export class MarkdownFileContentService {
private readonly markDownService: MarkdownService;
private readonly markdownHeaderService: MarkdownHeaderService;
private readonly htmlService: HTMLService;
private readonly codeBlockService: CodeBlockService;
private readonly markDownLinkRegEx: RegExp;
private readonly singleLinkRegEx: RegExp;
private readonly linkTagRegEx: RegExp;
private readonly htmlArrow = "🡒";

Expand All @@ -19,7 +20,7 @@ export class MarkdownFileContentService {
this.markdownHeaderService = new MarkdownHeaderService();
this.htmlService = new HTMLService();
this.codeBlockService = new CodeBlockService();
this.markDownLinkRegEx = /\[(.*?)\]\((.*?)\)/g;
this.singleLinkRegEx = /\[.+?\]\(.+?\)/g;
this.linkTagRegEx = /<a\s+name\s*=\s*'(.+)'><\/a\s*>/;
}

Expand Down Expand Up @@ -61,6 +62,9 @@ export class MarkdownFileContentService {
// Process all headers to change them to the appropriate size
fileContent = this.markdownHeaderService.processHeaders(fileContent);

// Process all fragments in markdown links 👉🏼 [stuff](Item1.Item2#Item1.Item2) gets converted to [stuff](Item1.Item2#item2)
fileContent = this.processMarkdownLinkFragments(fileContent);

// Extract the name of the file without its extension
const baseName = basename(filePath);
const extension = extname(filePath);
Expand All @@ -81,7 +85,7 @@ export class MarkdownFileContentService {
const line = fileLines[i];

const linkTagMatches = line.match(this.linkTagRegEx);
const isLinkTag: boolean = linkTagMatches != null && linkTagMatches.length > 0;
const isLinkTag: boolean = linkTagMatches !== null && linkTagMatches.length > 0;

if (isLinkTag) {
let nameValue: string = this.htmlService.getNameAttrValue(line);
Expand All @@ -94,6 +98,60 @@ export class MarkdownFileContentService {
return Utils.toString(fileLines);
}

/**
* Processes the given {@link fileContent} by finding all markdown links that contain fragments
* in the url and changes the fragment by setting it to lowercase, replacing spaces with hyphens,
* and removing all api namespace prefixes.
* @param fileContent The content of the file to process.
* @returns The processed file content.
*/
private processMarkdownLinkFragments(fileContent: string): string {
const fileLines: string[] = Utils.toLines(fileContent);

const service = new ProcessFragmentService();

for (let i = 0; i < fileLines.length; i++) {
fileLines[i] = service.processFragments(fileLines[i]);
}

return `${fileLines.join("\n")}\n`;
}

/**
* Updates the url fragment in the given {@link markdownLink} if it contains some hover text
* and the fragment contains periods which means it is a fully qualified type name.
* @param markdownLink The markdown link.
* @returns The updated markdown link.
*/
private updateMarkdownLinkFragment(markdownLink: string): string {
const hoverTextRegex = /'.+?'/gm;

const linkText = this.markDownService.extractLinkText(markdownLink);
const fullUrl = this.markDownService.extractLinkUrl(markdownLink);
const containsHoverText = hoverTextRegex.test(fullUrl);
const containsFragment = fullUrl.includes("#");

if (containsHoverText && containsFragment) {
const [urlSection, hoverTextSection] = fullUrl.split(" ");

const [url, fragment] = urlSection.split("#");

if (fragment.includes(".")) {
const fragmentSections = fragment.split(".");
const fragmentIsGenericParam = fragment.includes("_T_");

const newFragment = fragmentIsGenericParam
? "type-parameters"
: fragmentSections[fragmentSections.length - 1].toLowerCase().replace(" ", "-");

const newUrl = `${url}#${newFragment} ${hoverTextSection}`;
return this.markDownService.createLink(linkText, newUrl);
}
}

return "";
}

private processHeaderAngleBrackets(fileContent: string): string {
if (Utils.isNothing(fileContent)) {
return "";
Expand All @@ -120,8 +178,8 @@ export class MarkdownFileContentService {
for (let i = 0; i < fileLines.length; i++) {
const line = fileLines[i];

const notEmpty = line != "";
const containsAngles: boolean = line.indexOf("<") != -1 && line.indexOf(">") != -1;
const notEmpty = line !== "";
const containsAngles: boolean = line.indexOf("<") !== -1 && line.indexOf(">") !== -1;
const notCodeBlock = !this.codeBlockService.inAnyCodeBlocks(codeBlocks, i);
const notHeader = !this.markDownService.isHeaderLine(line);
const notHTMLLink = !this.htmlService.isHTMLLink(line);
Expand Down Expand Up @@ -166,9 +224,9 @@ export class MarkdownFileContentService {
for (let i = 0; i < fileLines.length; i++) {
const line = fileLines[i];

if (line.lastIndexOf("| |") != -1) {
if (line.lastIndexOf("| |") !== -1) {
fileLines[i] = line.replace("| |", "|");
} else if (line.indexOf("| :--- |") != -1) {
} else if (line.indexOf("| :--- |") !== -1) {
fileLines[i] = "| :--- |";
}
}
Expand All @@ -181,7 +239,7 @@ export class MarkdownFileContentService {
newUrl: string,
predicate: ((text: string, url: string) => boolean) | null = null,
): string {
const matches = fileContent.match(this.markDownLinkRegEx);
const matches = fileContent.match(this.singleLinkRegEx);
matches?.forEach((link) => {
const text: string = this.markDownService.extractLinkText(link);
const url: string = this.markDownService.extractLinkUrl(link);
Expand Down
16 changes: 13 additions & 3 deletions .github/cicd/core/services/MarkdownService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class MarkdownService {
// /(?!.*\(\()/ is 2 consecutive ( parenthesis
// /(?!.*\)\))/ is 2 consecutive ) parenthesis

this.fullMarkdownLinkRegEx = /(?!.*\[\[)(?!.*\]\])(?!.*\(\()(?!.*\)\))(\[.+\]\(.+\))/;
this.fullMarkdownLinkRegEx = /\[.+?\]\(.+?\)/gm;
this.headerLineRegEx = /^#+ .+$/g;

this.newLine = Utils.isWindows() ? "\r\n" : "\n";
Expand All @@ -32,7 +32,7 @@ export class MarkdownService {

// TODO: The URL link is going to have to change somehow
// once we start having versions. Maybe bring in a version parameter somehow
return `[${text}](<${url}>)`;
return `[${text}](${url})`;
}

public prefixUrl(markDownLink: string, prefix: string): string {
Expand All @@ -56,6 +56,17 @@ export class MarkdownService {
return `[${text}](${newUrl})`;
}

/**
* Extracts all markdown links from the given {@link value}.
* @param value The value to extract markdown links from.
* @returns The markdown links.
*/
public extractMarkdownLink(value: string): string | undefined {
const matches = Array.from(value.matchAll(this.fullMarkdownLinkRegEx), (match) => match[0]);

return matches.length > 0 ? matches[0] : undefined;
}

public extractLinkText(markDownLink: string): string {
if (!this.containsMarkdownLink(markDownLink)) {
throw new Error(`The markdown link is invalid.\`n${markDownLink}`);
Expand Down Expand Up @@ -199,7 +210,6 @@ export class MarkdownService {
return matches != null && matches.length > 0;
}


public isCodeBlockStartLine(line: string): boolean {
if (Utils.isNothing(line)) {
return false;
Expand Down
118 changes: 118 additions & 0 deletions .github/cicd/core/services/ProcessFragmentService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* Processes URL fragments inside of markdown link URL text by converting them to lowercase and extracting only
* the member name from the fully qualified C# type.
*/
export class ProcessFragmentService {
private readonly leftBracket = "[";
private readonly leftParen = "(";
private readonly rightParen = ")";

/**
* Processes all markdown links in the given {@link text}.
* @param text The text to process.
* @returns The processed text.
*/
public processFragments(text: string): string {
const links: string[] = this.extractLinks(text);

// Create all of the new links if they contain fragments
links.forEach((link) => {
const hoverTextRegex = /'.+'/g;

const linkText = this.extractLinkText(link);
let linkUrl = this.extractLinkUrl(link);

if (linkUrl.includes("#")) {
let hoverText = hoverTextRegex.exec(linkUrl)?.[0] ?? "";
hoverText = hoverText === "" ? "" : ` ${hoverText}`;

linkUrl = linkUrl.replace(hoverText, "").trimEnd();

let [fragPrefix, fragment] = linkUrl.split("#");

fragment = fragment.includes(".") ? fragment.split(".").pop() ?? "" : fragment;

fragment = fragment.toLowerCase();

const newLink = `[${linkText}](${fragPrefix}#${fragment}${hoverText})`;

text = text.replaceAll(link, newLink);
}
});

return text;
}

/**
* Extracts all links from the given {@link text}.
* @param text The text to extract links from.
* @returns The extracted links.
*/
private extractLinks(text: string): string[] {
const links: string[] = [];
const link: string[] = [];
let insideLink = false;
let parenNestLevel = 0;

for (let i = 0; i < text.length; i++) {
const char = text[i];

switch (char) {
case this.leftBracket:
insideLink = true;
link.push(char);
continue;
case this.leftParen:
parenNestLevel = i < text.length - 1 ? parenNestLevel + 1 : parenNestLevel;
break;
case this.rightParen:
link.push(char);
parenNestLevel -= 1;

// If the paren next level is 0, then we are not inside a link
insideLink = parenNestLevel > 0;

if (!insideLink) {
links.push(link.join(""));

// Empty the array
link.length = 0;
}
continue;
}

if (insideLink) {
link.push(char);
}
}

return links;
}

/**
* Extracts the link text from the given {@link markdownLink}.
* @param markdownLink The link to extract the text from.
* @returns The extracted link text.
*/
private extractLinkText(markdownLink: string): string {
const linkTextRegex = /\[.+?\]/g;
const result = (linkTextRegex.exec(markdownLink)?.[0] ?? "").replace("[", "").replace("]", "");

return result;
}

/**
* Extracts the link URL from the given {@link markdownLink}.
* @param markdownLink The link to extract the URL from.
* @returns The extracted link URL.
*/
private extractLinkUrl(markdownLink: string): string {
const linkUrlRegex = /\(.+\)/;
let result = linkUrlRegex.exec(markdownLink)?.[0] ?? "";

result = result.startsWith("(") ? result.substring(1) : result;
result = result.endsWith(")") ? result.substring(0, result.length - 1) : result;

return result;
}
}
1 change: 0 additions & 1 deletion .github/cicd/playground.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@

const _rootRepoDirPath = Deno.args[0];
const _token = Deno.args[1];
6 changes: 2 additions & 4 deletions .github/cicd/scripts/generate-new-api-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const apiVersionType: GenerateSrcType = "api version";

if (isInteractive) {
// Ask for an API version or branch name
generateSrcType = <GenerateSrcType>(await Select.prompt({
generateSrcType = <GenerateSrcType> (await Select.prompt({
message: "Enter the type of source you want to generate from.",
options: [branchType, apiVersionType],
}));
Expand All @@ -49,7 +49,7 @@ if (isInteractive) {
tagOrBranch = await Input.prompt({
message: "Enter the branch name",
});
} else if(generateSrcType === "api version") {
} else if (generateSrcType === "api version") {
const token = Deno.env.get("CICD_TOKEN");

if (Utils.isNothing(token)) {
Expand All @@ -62,13 +62,11 @@ if (isInteractive) {

const tags = (await tagClient.getAllTags()).map((tag) => tag.name)
.filter((tag) => Utils.validPreviewVersion(tag) || Utils.validProdVersion(tag));


tagOrBranch = await Select.prompt({
message: "Select a release version",
options: tags,
});

} else {
console.error("Unknown source type selected.");
Deno.exit();
Expand Down
7 changes: 4 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
".github/cicd/"
],
"deno.config": ".github/cicd/deno.json",
"files.eol": "\n",
"cSpell.words": [
"cicd",
"clsx",
Expand All @@ -26,9 +27,9 @@
"node_modules": true,
"VelaptorDocs.code-workspace": true,
"build/": true,
".github/cicd/": false,
".config": false,
"RepoSrc/": true,
".github/cicd/": true,
".config": true,
"**/RepoSrc": true,
"*.lock": true,
"SampleProjects": true,
},
Expand Down
4 changes: 2 additions & 2 deletions .github/cicd/deno.json → deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"build": "deno run --allow-read --allow-env --allow-sys --allow-run ./scripts/deno-check.ts"
},
"fmt": {
"include": ["**/*.ts"],
"include": [".github/cicd/**/*.ts"],
"exclude": [
"**/.config/",
"**/.docusaurus/",
Expand All @@ -30,7 +30,7 @@
},
"nodeModulesDir": false,
"lint": {
"include": ["**/*.ts"],
"include": [".github/cicd/**/*.ts"],
"exclude": [
"**/*.json",
"**/*.md",
Expand Down
Loading
Loading