diff --git a/app/packages/rehype-embedded-github-code/index.ts b/app/packages/rehype-embedded-github-code/index.ts deleted file mode 100644 index c8e9016..0000000 --- a/app/packages/rehype-embedded-github-code/index.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Element, Root } from "hast"; -import type { Plugin } from "unified"; -import { visit } from "unist-util-visit"; -import { - buildRequestURL, - extractCodeByLines, - extractFilePathFromURL, - extractLinesFromURL, - extractRefFromURL, - extractRepoNameFromURL, -} from "./util"; - -interface EmbeddedGithubCode { - url: string; - index: number; - parent: Root | Element; -} - -const rehypeEmbeddedGithubCode: Plugin<[], Root> = () => { - const embeddedGithubCodes: EmbeddedGithubCode[] = []; - - return async (tree) => { - visit(tree, "element", (node, index, parent) => { - if ( - node.tagName !== "a" || - node.properties["data-remark-target"] !== "remark-embedded-github-code" || - typeof node.properties.href !== "string" - ) { - return; - } - - if (index === undefined || parent === undefined) { - return; - } - - if (parent.type !== "root" && parent.type !== "element") { - return; - } - - embeddedGithubCodes.push({ - url: node.properties.href, - index, - parent, - }); - }); - - await Promise.all( - embeddedGithubCodes.map(async (embeddedGithubCode) => { - const repoName = extractRepoNameFromURL(embeddedGithubCode.url); - const ref = extractRefFromURL(embeddedGithubCode.url); - const path = extractFilePathFromURL(embeddedGithubCode.url); - const lines = extractLinesFromURL(embeddedGithubCode.url); - - const requestUrl = buildRequestURL(repoName, ref, path); - - const response = await fetch(requestUrl, { - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${process.env.GITHUB_PAT}`, - "X-GitHub-Api-Version": "2022-11-28", - }, - }); - const content = (await response.json()) as { content: string }; - - const codeBase64 = content.content; - let code = Buffer.from(codeBase64, "base64").toString("utf-8"); - if (lines) { - code = extractCodeByLines(code, lines); - } - - const codeElement: Element = { - type: "element", - tagName: "code", - properties: { - className: "", - }, - children: [ - { - type: "text", - value: code, - }, - ], - }; - - const preElement: Element = { - type: "element", - tagName: "pre", - children: [codeElement], - properties: {}, - }; - - embeddedGithubCode.parent.children[embeddedGithubCode.index] = preElement; - }), - ); - }; -}; - -export { rehypeEmbeddedGithubCode }; diff --git a/app/packages/rehype-embedded-github-code/util.ts b/app/packages/rehype-embedded-github-code/util.ts deleted file mode 100644 index 2801510..0000000 --- a/app/packages/rehype-embedded-github-code/util.ts +++ /dev/null @@ -1,62 +0,0 @@ -const extractRepoNameFromURL = (url: string): string => { - const base = url.replace("https://github.com/", ""); - const urlParts = base.split("/"); - return urlParts.slice(0, 2).join("/"); -}; - -const extractFilePathFromURL = (url: string): string => { - const base = url.replace("https://github.com/", ""); - const urlParts = base.split("/"); - const result = urlParts.slice(4).join("/"); - const hashIndex = result.indexOf("#"); - if (hashIndex === -1) { - return result; - } - return result.slice(0, hashIndex); -}; - -const extractRefFromURL = (url: string): string => { - const base = url.replace("https://github.com/", ""); - const urlParts = base.split("/"); - return urlParts[3]; -}; - -type LineNumbers = { - startLine: number; - endLine: number; -}; - -const extractLinesFromURL = (url: string): LineNumbers | undefined => { - const base = url.replace("https://github.com/", ""); - const urlParts = base.split("/"); - 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 buildRequestURL = (repo: string, ref: string, path: string): string => { - return `https://api.github.com/repos/${repo}/contents/${path}?ref=${ref}`; -}; - -const extractCodeByLines = (code: string, lines: LineNumbers): string => { - const codeLines = code.split("\n"); - return codeLines.slice(lines.startLine - 1, lines.endLine).join("\n"); -}; - -export { - extractRepoNameFromURL, - extractFilePathFromURL, - extractRefFromURL, - extractLinesFromURL, - buildRequestURL, - extractCodeByLines, -}; diff --git a/app/packages/rehype-github-embed/index.ts b/app/packages/rehype-github-embed/index.ts new file mode 100644 index 0000000..76a2c0f --- /dev/null +++ b/app/packages/rehype-github-embed/index.ts @@ -0,0 +1,112 @@ +import type { Element, Root } from "hast"; +import type { Plugin, Transformer } from "unified"; +import { visit } from "unist-util-visit"; +import { + type Lines, + buildRequestURL, + createCodeBlock, + createErrBlock, + extractCodeByLines, + extractRepoDataFromURL, +} from "./utils"; + +type Option = { + githubPAT: string; +}; + +const rehypeGithubEmbed: Plugin = (options): Transformer => { + const { githubPAT } = options; + + const transform: Transformer = async (tree) => { + const githubEmbedPromises: Promise[] = []; + + const headers = { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${githubPAT}`, + "X-GitHub-Api-Version": "2022-11-28", + }; + + visit(tree, "element", (node, index, parent) => { + const regex = new RegExp(/https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/blob\/[\w.-]+(\/[^\s]*)?/g); + + if ( + node.tagName !== "a" || + node.children.length === 0 || + node.children[0].type !== "text" || + !regex.test(node.children[0].value) + ) { + return; + } + + if (index === undefined || parent === undefined) { + return; + } + + if (parent.type === "mdxJsxFlowElement" || parent.type === "mdxJsxTextElement") { + return; + } + + type Props = { + parent: Root | Element; + href: string; + owner: string; + repoName: string; + ref: string; + path: string; + lines: Lines | undefined; + }; + + 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, + }); + if (!response.ok) { + throw new Error(`Failed to fetch code from GitHub: ${response.status}`); + } + const data = (await response.json()) as { + content: string; + }; + const codeBase64 = data.content; + const code = extractCodeByLines(Buffer.from(codeBase64, "base64").toString("utf-8"), lines); + + const title = `${repoName}/${path}${ + lines + ? `#${lines.startLine && lines.endLine ? `L${lines.startLine}-L${lines.endLine}` : `L${lines.startLine}`}` + : "" + }`; + + const element = createCodeBlock(title, href, code); + parent.children[index] = element; + } catch (error) { + const element = createErrBlock(`Something went wrong while rendering the code from GitHub`); + parent.children[index] = element; + console.error("Something went wrong", { cause: error }); + } + }; + + 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); + }; + + return transform; +}; + +export { rehypeGithubEmbed }; diff --git a/app/packages/rehype-github-embed/utils.ts b/app/packages/rehype-github-embed/utils.ts new file mode 100644 index 0000000..55f48de --- /dev/null +++ b/app/packages/rehype-github-embed/utils.ts @@ -0,0 +1,80 @@ +import { Element } from "hast"; +import { h } from "hastscript"; + +const extractOwner = (pathname: string): string => { + const pathnameParts = pathname.split("/"); + return pathnameParts[1]; +}; + +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 = (pathname: string): string => { + const pathnameParts = pathname.split("/"); + return pathnameParts[4]; +}; + +export type Lines = { + startLine: number; + endLine?: number; +}; + +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 = new URL(url); + return { + 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"); + + if (lines.startLine && lines.endLine) { + return codeLines.slice(lines.startLine - 1, lines.endLine).join("\n"); + } + + return codeLines[lines.startLine - 1]; +}; + +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): Element => { + return h("div", [ + h("span", { class: ["rehype-embedded-github-code-title"] }, [ + h("a", { href: href }, [h(null, [{ type: "text", value: title }])]), + ]), + h("pre", [h("code", [h(null, [{ type: "text", value: code }])])]), + ]); +}; + +const createErrBlock = (message: string): Element => { + return h("div", [h("span", [h(null, [{ type: "text", value: message }])])]); +}; + +export { extractRepoDataFromURL, buildRequestURL, extractCodeByLines, createCodeBlock, createErrBlock }; diff --git a/app/packages/remark-embedded-github-code/index.ts b/app/packages/remark-embedded-github-code/index.ts deleted file mode 100644 index 4fa6bb7..0000000 --- a/app/packages/remark-embedded-github-code/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Root } from "mdast"; -import type { Plugin } from "unified"; -import { visit } from "unist-util-visit"; - -const remarkEmbeddedGithubCode: Plugin<[], Root> = () => { - //TODO: check this regex - const regex = /https:\/\/github\.com\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+\/blob\/[a-zA-Z0-9._-]+\/?[^\s]*/g; - return (tree) => { - visit(tree, "link", (node, _, parent) => { - if (node.url && typeof node.url === "string") { - const matches = node.url.match(regex); - if (matches) { - node.data = { - hProperties: { - "data-remark-target": "remark-embedded-github-code", - }, - }; - - if (parent && parent.type === "paragraph") { - parent.data = { - hName: "div", - }; - } - } - } - }); - }; -}; - -export { remarkEmbeddedGithubCode }; diff --git a/app/routes/_renderer.tsx b/app/routes/_renderer.tsx index 48e181f..1626e39 100644 --- a/app/routes/_renderer.tsx +++ b/app/routes/_renderer.tsx @@ -46,6 +46,7 @@ export default jsxRenderer(({ children, title, description }) => { {/* global css */} +