diff --git a/app/packages/rehype-github-embed/index.ts b/app/packages/rehype-github-embed/index.ts index aa080d7..62a167b 100644 --- a/app/packages/rehype-github-embed/index.ts +++ b/app/packages/rehype-github-embed/index.ts @@ -1,7 +1,14 @@ import type { Element, Root } from "hast"; import type { Plugin, Transformer } from "unified"; import { visit } from "unist-util-visit"; -import { type Lines, buildRequestURL, createCodeBlock, extractCodeByLines, extractRepoDataFromURL } from "./utils"; +import { + type Lines, + buildRequestURL, + createCodeBlock, + createErrBlock, + extractCodeByLines, + extractRepoDataFromURL, +} from "./utils"; type Option = { githubPAT: string; @@ -11,10 +18,6 @@ const rehypeGithubEmbed: Plugin = (options): Transformer = const { githubPAT } = options; const transform: Transformer = async (tree) => { - const regex = new RegExp( - /https:\/\/github\.com\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+\/blob\/[a-zA-Z0-9._-]+\/?[^\s]*/g, - ); - const githubEmbedPromises: Promise[] = []; const headers = { @@ -24,11 +27,15 @@ const rehypeGithubEmbed: Plugin = (options): Transformer = }; visit(tree, "element", (node, index, parent) => { + const regex = new RegExp( + /https:\/\/github\.com\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+\/blob\/[a-zA-Z0-9._-]+\/?[^\s]*/g, + ); + if ( node.tagName !== "a" || - node.properties.href === undefined || - typeof node.properties.href !== "string" || - !regex.test(node.properties.href) + node.children.length === 0 || + node.children[0].type !== "text" || + !regex.test(node.children[0].value) ) { return; } @@ -37,39 +44,69 @@ const rehypeGithubEmbed: Plugin = (options): Transformer = return; } + if (parent.type === "mdxJsxFlowElement" || parent.type === "mdxJsxTextElement") { + return; + } + type Props = { - node: Element; + parent: Root | Element; href: string; + owner: string; repoName: string; ref: string; path: string; lines?: Lines; }; - const githubEmbedPromise = async ({ repoName, ref, href, path, lines }: Props): Promise => { - const requestUrl = buildRequestURL(repoName, ref, path); + const githubEmbedPromise = async ({ parent, owner, repoName, ref, href, path, lines }: Props): Promise => { + const requestUrl = buildRequestURL(owner, repoName, ref, path); + try { - const response = await fetch(requestUrl, { headers }); + const response = await fetch(requestUrl, { + headers, + }); if (!response.ok) { + if (response.status === 404) { + const element = createErrBlock("404 Not Found"); + parent.children[index] = element; + return; + } + throw new Error(`Failed to fetch code from GitHub: ${response.status}`); } - const content = (await response.json()) as { content: string }; + const content = (await response.json()) as { + content: string; + }; const codeBase64 = content.content; const code = extractCodeByLines(Buffer.from(codeBase64, "base64").toString("utf-8"), lines); - const title = `${repoName}/${path}${lines ? `#L${lines.startLine}-L${lines.endLine}` : ""}`; - const codeBlockElement = createCodeBlock(title, href, code); + const title = `${repoName}/${path}${ + lines + ? `#${lines.startLine && lines.endLine ? `L${lines.startLine}-L${lines.endLine}` : `L${lines.startLine}`}` + : "" + }`; - parent.children[index] = codeBlockElement; + const element = createCodeBlock(title, href, code); + parent.children[index] = element; } catch (error) { throw new Error("Failed to fetch code from GitHub", { cause: error }); } }; - const nodeHref = node.properties.href; - const { repoName, ref, path, lines } = extractRepoDataFromURL(nodeHref); - - githubEmbedPromises.push(githubEmbedPromise({ node, repoName, href: nodeHref, ref, path, lines })); + const nodeHref = node.children[0].value; + const { repoName, owner, ref, path, lines } = extractRepoDataFromURL(nodeHref); + + githubEmbedPromises.push( + githubEmbedPromise({ + parent, + repoName, + owner, + href: nodeHref, + ref, + path, + lines, + }), + ); }); await Promise.all(githubEmbedPromises); diff --git a/app/packages/rehype-github-embed/utils.ts b/app/packages/rehype-github-embed/utils.ts index a35c65e..704ce5f 100644 --- a/app/packages/rehype-github-embed/utils.ts +++ b/app/packages/rehype-github-embed/utils.ts @@ -1,61 +1,66 @@ import { h } from "hastscript"; -const extractRepoName = (urlParts: string[]): string => { - return urlParts.slice(0, 2).join("/"); +const extractOwner = (pathname: string): string => { + const pathnameParts = pathname.split("/"); + return pathnameParts[1]; }; -const extractFilePath = (urlParts: string[]): string => { - const result = urlParts.slice(4).join("/"); - const hashIndex = result.indexOf("#"); - if (hashIndex === -1) { - return result; - } - return result.slice(0, hashIndex); +const extractRepoName = (pathname: string): string => { + const pathnameParts = pathname.split("/"); + return pathnameParts[2]; +}; + +const extractFilePath = (pathname: string): string => { + const pathnameParts = pathname.split("/"); + return pathnameParts.slice(5).join("/"); }; -const extractRef = (urlParts: string[]): string => { - return urlParts[3]; +const extractRef = (pathname: string): string => { + const pathnameParts = pathname.split("/"); + return pathnameParts[4]; }; export type Lines = { startLine: number; - endLine: number; + endLine?: number; }; -const extractLines = (urlParts: string[]): Lines | undefined => { - const result = urlParts.slice(4).join("/"); - const hashIndex = result.indexOf("#"); - if (hashIndex === -1) { - return; - } - const hash = result.slice(hashIndex + 1); - const numbers = hash.match(/\d+/g); - if (!numbers) return; - return { - startLine: Number.parseInt(numbers[0]), - endLine: Number.parseInt(numbers[1]), - }; +const extractLines = (hash: string): Lines | undefined => { + const matches = hash.match(/\d+/g); + if (!matches) return; + + return matches[0] && matches[1] + ? { + startLine: Number.parseInt(matches[0]), + endLine: Number.parseInt(matches[1]), + } + : { startLine: Number.parseInt(matches[0]) }; }; const extractRepoDataFromURL = (url: string) => { - const base = url.replace("https://github.com/", ""); - const urlParts = base.split("/"); + const base = new URL(url); return { - repoName: extractRepoName(urlParts), - ref: extractRef(urlParts), - path: extractFilePath(urlParts), - lines: extractLines(urlParts), + repoName: extractRepoName(base.pathname), + owner: extractOwner(base.pathname), + ref: extractRef(base.pathname), + path: extractFilePath(base.pathname), + lines: extractLines(base.hash), }; }; const extractCodeByLines = (code: string, lines: Lines | undefined): string => { if (!lines) return code; const codeLines = code.split("\n"); - return codeLines.slice(lines.startLine - 1, lines.endLine).join("\n"); + + if (lines.startLine && lines.endLine) { + return codeLines.slice(lines.startLine - 1, lines.endLine).join("\n"); + } + + return codeLines[lines.startLine - 1]; }; -const buildRequestURL = (repo: string, ref: string, path: string): string => { - return `https://api.github.com/repos/${repo}/contents/${path}?ref=${ref}`; +const buildRequestURL = (owner: string, repoName: string, ref: string, path: string): string => { + return `https://api.github.com/repos/${owner}/${repoName}/contents/${path}?ref=${ref}`; }; const createCodeBlock = (title: string, href: string, code: string) => { @@ -67,4 +72,8 @@ const createCodeBlock = (title: string, href: string, code: string) => { ]); }; -export { extractRepoDataFromURL, buildRequestURL, extractCodeByLines, createCodeBlock }; +const createErrBlock = (message: string) => { + return h("div", [h("span", [h(null, [{ type: "text", value: message }])])]); +}; + +export { extractRepoDataFromURL, buildRequestURL, extractCodeByLines, createCodeBlock, createErrBlock };