diff --git a/package-lock.json b/package-lock.json
index b102eea..3a3c2ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,14 +1,15 @@
{
"name": "react-github-permalink",
- "version": "1.11.0",
+ "version": "1.11.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "react-github-permalink",
- "version": "1.11.0",
+ "version": "1.11.1",
"license": "MIT",
"dependencies": {
+ "lz-string": "^1.5.0",
"react-responsive": "^10.0.0",
"react-syntax-highlighter": "^15.5.0"
},
@@ -25,6 +26,7 @@
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
+ "@types/lz-string": "^1.3.34",
"@types/node": "^20",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
@@ -5241,6 +5243,13 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/lz-string": {
+ "version": "1.3.34",
+ "resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz",
+ "integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/mdx": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz",
@@ -13603,7 +13612,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
- "dev": true,
+ "license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
}
diff --git a/package.json b/package.json
index 814330a..711c2dc 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"release": "changeset publish"
},
"dependencies": {
+ "lz-string": "^1.5.0",
"react-responsive": "^10.0.0",
"react-syntax-highlighter": "^15.5.0"
},
@@ -44,6 +45,7 @@
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
+ "@types/lz-string": "^1.3.34",
"@types/node": "^20",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 77ec7d0..e266868 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,3 +1,5 @@
+import { TypeScriptPlayground } from "@/library/TypeScriptPlayground/TypeScriptPlayground";
+import "@/library/GithubPermalink/github-permalink.css";
export default function Home() {
return (
@@ -5,8 +7,15 @@ export default function Home() {
React Github Permalink
+
This library now supports TypeScript Playground links!
- See the
Storybook
+
Example: TypeScript Playground
+
+
+
+
+
+ See the
Storybook for more examples
);
diff --git a/src/library/TypeScriptPlayground/TypeScriptPlayground.stories.tsx b/src/library/TypeScriptPlayground/TypeScriptPlayground.stories.tsx
new file mode 100644
index 0000000..dd012a7
--- /dev/null
+++ b/src/library/TypeScriptPlayground/TypeScriptPlayground.stories.tsx
@@ -0,0 +1,33 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { TypeScriptPlayground } from "./TypeScriptPlayground";
+import { GithubPermalinkProvider } from "../config/GithubPermalinkContext";
+import "../GithubPermalink/github-permalink.css";
+
+const meta: Meta = {
+ component: TypeScriptPlayground,
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ render: () => (
+
+ ),
+};
+
+export const SimpleCode: Story = {
+ render: () => (
+
+ ),
+};
+
+export const WithProvider: Story = {
+ render: () => (
+
+
+
+ ),
+};
diff --git a/src/library/TypeScriptPlayground/TypeScriptPlayground.tsx b/src/library/TypeScriptPlayground/TypeScriptPlayground.tsx
new file mode 100644
index 0000000..419dd66
--- /dev/null
+++ b/src/library/TypeScriptPlayground/TypeScriptPlayground.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import { useContext, useEffect, useState } from "react";
+import { TypeScriptPlaygroundDataResponse, GithubPermalinkContext } from "../config/GithubPermalinkContext";
+import { TypeScriptPlaygroundBase, TypeScriptPlaygroundBaseProps } from "./TypeScriptPlaygroundBase";
+
+type TypeScriptPlaygroundProps = Omit & { playgroundUrl: string };
+
+export function TypeScriptPlayground(props: TypeScriptPlaygroundProps) {
+ const { playgroundUrl } = props;
+ const [data, setData] = useState(null as null | TypeScriptPlaygroundDataResponse);
+ const { getTypeScriptPlaygroundFn, githubToken, onError } = useContext(GithubPermalinkContext);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ getTypeScriptPlaygroundFn(playgroundUrl, githubToken, onError).then((v) => {
+ setIsLoading(false);
+ setData(v);
+ })
+ }, [getTypeScriptPlaygroundFn, githubToken, onError, playgroundUrl])
+
+ if (isLoading) {
+ return null;
+ }
+ if (!data) {
+ throw new Error("Loading is complete, but no data was returned.")
+ }
+
+ return
+}
diff --git a/src/library/TypeScriptPlayground/TypeScriptPlaygroundBase.stories.tsx b/src/library/TypeScriptPlayground/TypeScriptPlaygroundBase.stories.tsx
new file mode 100644
index 0000000..b8a093d
--- /dev/null
+++ b/src/library/TypeScriptPlayground/TypeScriptPlaygroundBase.stories.tsx
@@ -0,0 +1,98 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { TypeScriptPlaygroundBase } from "./TypeScriptPlaygroundBase";
+import "../GithubPermalink/github-permalink.css";
+
+const meta: Meta = {
+ component: TypeScriptPlaygroundBase,
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const WithData: Story = {
+ render: () => (
+
+ ),
+};
+
+export const ErrorState: Story = {
+ render: () => (
+
+ ),
+};
+
+export const LongCode: Story = {
+ render: () => (
+ & { id: string };",
+ "",
+ "type ColumnData = {",
+ " key: TKey",
+ " renderData: (value: TRowData[TKey]) => string;",
+ "}",
+ "",
+ "function processTable(row: Array, column: Array>) {",
+ "",
+ "}",
+ "",
+ "",
+ "processTable(",
+ " [{",
+ " id: \"123\",",
+ " a: 1,",
+ " b: {",
+ " x: 1,",
+ " y: 2",
+ " }",
+ " }], [{",
+ " key: \"a\",",
+ " renderData: (value) => {",
+ " // (parameter) value: number | {",
+ " // x: number;",
+ " // y: number;",
+ " // }",
+ " return `${value}`;",
+ " }",
+ " },",
+ " {",
+ " key: \"b\",",
+ " renderData: (value) => {",
+ "",
+ " // (parameter) value: number | {",
+ " // x: number;",
+ " // y: number;",
+ " // }",
+ " //Property 'x' does not exist on type 'number | { x: number; y: number; }'.",
+ " return `${value.x},${value.y}`;",
+ " }",
+ " },",
+ "]);"
+ ],
+ startLine: 44,
+ endLine: 8,
+ status: "ok"
+ }}
+ />
+ ),
+};
diff --git a/src/library/TypeScriptPlayground/TypeScriptPlaygroundBase.tsx b/src/library/TypeScriptPlayground/TypeScriptPlaygroundBase.tsx
new file mode 100644
index 0000000..1eb1c40
--- /dev/null
+++ b/src/library/TypeScriptPlayground/TypeScriptPlaygroundBase.tsx
@@ -0,0 +1,67 @@
+import { TypeScriptPlaygroundDataResponse } from "../config/GithubPermalinkContext";
+import { ErrorMessages } from "../ErrorMessages/ErrorMessages";
+import { PropsWithChildren } from "react";
+import { SyntaxHighlight } from "../SyntaxHighlight/SyntaxHighlight";
+import { CopyButton } from "../common/CopyButton/CopyButton";
+import { AvailableLanguagesPrism } from "../SyntaxHighlight/availableLanguagesPrism";
+
+export type TypeScriptPlaygroundBaseProps = {
+ className?: string;
+ playgroundUrl: string;
+ data: TypeScriptPlaygroundDataResponse;
+ language?: AvailableLanguagesPrism;
+}
+
+export function TypeScriptPlaygroundBase(props: TypeScriptPlaygroundBaseProps) {
+ const { data, playgroundUrl } = props;
+
+ if (data.status === "ok") {
+ const language = props.language ?? "typescript";
+ const clipboard = data.lines.join("\n");
+
+ // Determine which lines to highlight based on startLine and endLine
+ const startLineNumber = data.startLine ?? 1;
+
+ return
+ TypeScript Playground
+ {data.startLine != null && data.endLine != null && (
+ Lines {data.startLine} to {data.endLine}
+ )}
+ >}>
+
+
+ }
+
+ return
+
+
+}
+
+function TypeScriptPlaygroundInner(props: PropsWithChildren<{
+ header?: React.ReactNode
+ clipboard?: string;
+} & TypeScriptPlaygroundBaseProps>) {
+ const { clipboard } = props;
+
+ return
+
+
+
+
+ {clipboard &&
+
+
}
+
+ {props.children}
+
+}
diff --git a/src/library/TypeScriptPlayground/TypeScriptPlaygroundRsc.stories.tsx b/src/library/TypeScriptPlayground/TypeScriptPlaygroundRsc.stories.tsx
new file mode 100644
index 0000000..1601edb
--- /dev/null
+++ b/src/library/TypeScriptPlayground/TypeScriptPlaygroundRsc.stories.tsx
@@ -0,0 +1,18 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { TypeScriptPlaygroundRsc } from "./TypeScriptPlaygroundRsc";
+import "../GithubPermalink/github-permalink.css";
+
+const meta: Meta = {
+ component: TypeScriptPlaygroundRsc,
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ render: () => (
+
+ ),
+};
diff --git a/src/library/TypeScriptPlayground/TypeScriptPlaygroundRsc.tsx b/src/library/TypeScriptPlayground/TypeScriptPlaygroundRsc.tsx
new file mode 100644
index 0000000..c2e635a
--- /dev/null
+++ b/src/library/TypeScriptPlayground/TypeScriptPlaygroundRsc.tsx
@@ -0,0 +1,15 @@
+import { TypeScriptPlaygroundBase, TypeScriptPlaygroundBaseProps } from "./TypeScriptPlaygroundBase";
+import { githubPermalinkRscConfig } from "../config/GithubPermalinkRscConfig";
+
+type TypeScriptPlaygroundRscProps = Omit & { playgroundUrl: string };
+
+export async function TypeScriptPlaygroundRsc(props: TypeScriptPlaygroundRscProps) {
+ const { playgroundUrl } = props;
+ const getTypeScriptPlaygroundFn = githubPermalinkRscConfig.getTypeScriptPlaygroundFn();
+ const githubToken = githubPermalinkRscConfig.getGithubToken();
+ const onError = githubPermalinkRscConfig.getOnError();
+
+ const data = await getTypeScriptPlaygroundFn(playgroundUrl, githubToken, onError);
+
+ return
+}
diff --git a/src/library/config/BaseConfiguration.ts b/src/library/config/BaseConfiguration.ts
index f75651f..b451d77 100644
--- a/src/library/config/BaseConfiguration.ts
+++ b/src/library/config/BaseConfiguration.ts
@@ -1,5 +1,6 @@
import { defaultGetPermalinkFn } from "./defaultFunctions";
import { defaultGetIssueFn } from "./defaultFunctions";
+import { defaultGetTypeScriptPlaygroundFn } from "./defaultFunctions";
export type BaseConfiguration = {
@@ -11,6 +12,9 @@ export type BaseConfiguration = {
/** Function to provide issue data payload */
getIssueFn: typeof defaultGetIssueFn;
+ /** Function to provide TypeScript playground data payload */
+ getTypeScriptPlaygroundFn: typeof defaultGetTypeScriptPlaygroundFn;
+
/**
* A github personal access token - will be passed to the data fetching functions
*/
diff --git a/src/library/config/GithubPermalinkContext.tsx b/src/library/config/GithubPermalinkContext.tsx
index c9b7f57..e426ba7 100644
--- a/src/library/config/GithubPermalinkContext.tsx
+++ b/src/library/config/GithubPermalinkContext.tsx
@@ -3,6 +3,7 @@ import { PropsWithChildren, createContext } from "react";
import { BaseConfiguration } from "./BaseConfiguration";
import { defaultGetIssueFn } from "./defaultFunctions";
import { defaultGetPermalinkFn } from "./defaultFunctions";
+import { defaultGetTypeScriptPlaygroundFn } from "./defaultFunctions";
// Thanks ChatGPT
export type GithubPermalinkUrlInfo = {
@@ -56,6 +57,17 @@ export type GithubIssueLinkDataResponse = {
}
} | ErrorResponses;
+export type TypeScriptPlaygroundSuccessData = {
+ lines: Array;
+ startLine?: number;
+ startColumn?: number;
+ endLine?: number;
+ endColumn?: number;
+ status: "ok";
+}
+
+export type TypeScriptPlaygroundDataResponse = TypeScriptPlaygroundSuccessData | ErrorResponses;
+
@@ -63,12 +75,14 @@ export type GithubIssueLinkDataResponse = {
export const GithubPermalinkContext = createContext({
getDataFn: defaultGetPermalinkFn,
getIssueFn: defaultGetIssueFn,
+ getTypeScriptPlaygroundFn: defaultGetTypeScriptPlaygroundFn,
});
export function GithubPermalinkProvider(props: PropsWithChildren>) {
return
diff --git a/src/library/config/GithubPermalinkRscConfig.ts b/src/library/config/GithubPermalinkRscConfig.ts
index 0e66ded..2a123b7 100644
--- a/src/library/config/GithubPermalinkRscConfig.ts
+++ b/src/library/config/GithubPermalinkRscConfig.ts
@@ -1,9 +1,10 @@
import { BaseConfiguration } from "./BaseConfiguration";
-import { defaultGetIssueFn, defaultGetPermalinkFn } from "./defaultFunctions";
+import { defaultGetIssueFn, defaultGetPermalinkFn, defaultGetTypeScriptPlaygroundFn } from "./defaultFunctions";
const defaultConfiguration = {
getDataFn: defaultGetPermalinkFn,
getIssueFn: defaultGetIssueFn,
+ getTypeScriptPlaygroundFn: defaultGetTypeScriptPlaygroundFn,
};
class GithubPermalinkRscConfig {
@@ -23,6 +24,10 @@ class GithubPermalinkRscConfig {
return this.baseConfiguration.getIssueFn;
}
+ public getTypeScriptPlaygroundFn() {
+ return this.baseConfiguration.getTypeScriptPlaygroundFn;
+ }
+
public getGithubToken() {
return this.baseConfiguration.githubToken;
}
diff --git a/src/library/config/defaultFunctions.ts b/src/library/config/defaultFunctions.ts
index a057fed..e7c1755 100644
--- a/src/library/config/defaultFunctions.ts
+++ b/src/library/config/defaultFunctions.ts
@@ -1,7 +1,9 @@
import { GithubIssueLinkDataResponse } from "./GithubPermalinkContext";
-import { parseGithubIssueLink, parseGithubPermalinkUrl } from "../utils/urlParsers";
+import { parseGithubIssueLink, parseGithubPermalinkUrl, parseTypeScriptPlaygroundUrl } from "../utils/urlParsers";
import { GithubPermalinkDataResponse } from "./GithubPermalinkContext";
import { ErrorResponses } from "./GithubPermalinkContext";
+import { TypeScriptPlaygroundDataResponse } from "./GithubPermalinkContext";
+import * as LZString from "lz-string";
/**
* This is AI generated code from GitHub Copilot.
@@ -133,3 +135,32 @@ export function handleResponse(response: Response): ErrorResponses {
};
}
+export async function defaultGetTypeScriptPlaygroundFn(playgroundUrl: string, _githubToken?: string, onError?: (err: unknown) => void): Promise {
+ try {
+ const config = parseTypeScriptPlaygroundUrl(playgroundUrl);
+
+ // Decompress the code using lz-string
+ const decodedCode = LZString.decompressFromEncodedURIComponent(config.code);
+
+ if (!decodedCode) {
+ const error = "Failed to decompress TypeScript playground code: invalid or corrupted data";
+ onError?.(error);
+ return { status: "other-error" };
+ }
+
+ // Split the code into lines
+ const lines = decodedCode.split("\n");
+
+ return {
+ lines,
+ startLine: config.startLine,
+ startColumn: config.startColumn,
+ endLine: config.endLine,
+ endColumn: config.endColumn,
+ status: "ok"
+ };
+ } catch (error) {
+ onError?.(error);
+ return { status: "other-error" };
+ }
+}
diff --git a/src/library/export.ts b/src/library/export.ts
index 31db22b..942a37d 100644
--- a/src/library/export.ts
+++ b/src/library/export.ts
@@ -3,4 +3,6 @@ export * from "./GithubPermalink/GithubPermalink";
export * from "./GithubPermalink/GithubPermalinkBase";
export * from "./GithubIssueLink/GithubIssueLink";
export * from "./GithubIssueLink/GithubIssueLinkBase"
+export * from "./TypeScriptPlayground/TypeScriptPlayground";
+export * from "./TypeScriptPlayground/TypeScriptPlaygroundBase";
export * from "./config/GithubPermalinkContext";
diff --git a/src/library/rsc.ts b/src/library/rsc.ts
index ce35b3d..b565007 100644
--- a/src/library/rsc.ts
+++ b/src/library/rsc.ts
@@ -1,3 +1,4 @@
export * from "./GithubPermalink/GithubPermalinkRsc";
export * from "./config/GithubPermalinkRscConfig";
-export * from "./GithubIssueLink/GithubIssueLinkRsc";
\ No newline at end of file
+export * from "./GithubIssueLink/GithubIssueLinkRsc";
+export * from "./TypeScriptPlayground/TypeScriptPlaygroundRsc";
\ No newline at end of file
diff --git a/src/library/utils/urlParsers.test.ts b/src/library/utils/urlParsers.test.ts
index b8376ba..0e9bcf7 100644
--- a/src/library/utils/urlParsers.test.ts
+++ b/src/library/utils/urlParsers.test.ts
@@ -1,5 +1,5 @@
import { expect, test, it, describe } from 'vitest'
-import { parseGithubPermalinkUrl } from "./urlParsers";
+import { parseGithubPermalinkUrl, parseTypeScriptPlaygroundUrl } from "./urlParsers";
describe(parseGithubPermalinkUrl, () => {
it("behaves correctly for correct urls", () => {
@@ -32,3 +32,45 @@ describe(parseGithubPermalinkUrl, () => {
})
})
});
+
+describe(parseTypeScriptPlaygroundUrl, () => {
+ it("parses TypeScript playground URL with line numbers", () => {
+ expect(
+ parseTypeScriptPlaygroundUrl(
+ "https://www.typescriptlang.org/play/?ssl=44&ssc=1&pln=8&pc=1#code/C4TwDgpgBASg9gdwCIENgqgXlhAxnAJwBMAeAZ2AIEsA7AcwBooBXGgaxsRoD4oAyKAG8oVIgC4oFavSgBfANwAoRaEhQAwnAA2zALY1U6EgBV4yNBggAPYBBpEysRIZRNjAaQggo12-cdsXnAAZlCmzha82IKKUHFQgSASHl6x8QR2RBAELhIAFABuKDoQyWYuANopIAC6AJRYvFK0dEqyysGsuMBUcDRQYARwuBBkZMYoAEZaECblFj42mY7z6G6e3r7LCUGh4ebo3HlDCBIAggQEKCBzEYdM+Dr655fXJJpPBha3B65hG9xuA0Yop2spBsNRuMpjM8mk4hUYvFkSJxFAAEQARgATABmdEMeEolASTGElHxSYSJEUlFWUnk2nIpJQbFE5HtDk1JiI9nxRISdEoAl8uIZezZXJQQrFZgQBqYXg0plxAD0qulYBQV10EFsBAaRRKEhoekm2SgAB8hKKUeq6SazdklCr4vbmY7dOaCC7XVB7Zy-RlgMwCP0AAYAEkERrlsnDvopgbiskZcWVKIFGMmIqZ4qyOQs+Vj8saNttyPteS1Or12UNstKUFNXot1ozTPd8XpzadPorbo1Ht7rf7fv9GuTKvVAAUhpACKAoAByKzLqBEOCjZtwYCLKgUKB9KCqaDLlveq1CKA9i-OqAsu8+uTLgB0A7FetDEejJdfVlTX9G1fEB40TFEp1TRR6iUZRlCAA"
+ )
+ ).toEqual({
+ code: "C4TwDgpgBASg9gdwCIENgqgXlhAxnAJwBMAeAZ2AIEsA7AcwBooBXGgaxsRoD4oAyKAG8oVIgC4oFavSgBfANwAoRaEhQAwnAA2zALY1U6EgBV4yNBggAPYBBpEysRIZRNjAaQggo12-cdsXnAAZlCmzha82IKKUHFQgSASHl6x8QR2RBAELhIAFABuKDoQyWYuANopIAC6AJRYvFK0dEqyysGsuMBUcDRQYARwuBBkZMYoAEZaECblFj42mY7z6G6e3r7LCUGh4ebo3HlDCBIAggQEKCBzEYdM+Dr655fXJJpPBha3B65hG9xuA0Yop2spBsNRuMpjM8mk4hUYvFkSJxFAAEQARgATABmdEMeEolASTGElHxSYSJEUlFWUnk2nIpJQbFE5HtDk1JiI9nxRISdEoAl8uIZezZXJQQrFZgQBqYXg0plxAD0qulYBQV10EFsBAaRRKEhoekm2SgAB8hKKUeq6SazdklCr4vbmY7dOaCC7XVB7Zy-RlgMwCP0AAYAEkERrlsnDvopgbiskZcWVKIFGMmIqZ4qyOQs+Vj8saNttyPteS1Or12UNstKUFNXot1ozTPd8XpzadPorbo1Ht7rf7fv9GuTKvVAAUhpACKAoAByKzLqBEOCjZtwYCLKgUKB9KCqaDLlveq1CKA9i-OqAsu8+uTLgB0A7FetDEejJdfVlTX9G1fEB40TFEp1TRR6iUZRlCAA",
+ startLine: 44,
+ startColumn: 1,
+ endLine: 8,
+ endColumn: 1,
+ });
+ });
+
+ it("parses TypeScript playground URL without line numbers", () => {
+ expect(
+ parseTypeScriptPlaygroundUrl(
+ "https://www.typescriptlang.org/play/#code/PTAEAEFMCdoe2gZwFygEwGY"
+ )
+ ).toEqual({
+ code: "PTAEAEFMCdoe2gZwFygEwGY",
+ startLine: undefined,
+ startColumn: undefined,
+ endLine: undefined,
+ endColumn: undefined,
+ });
+ });
+
+ it("throws error for invalid TypeScript playground URL", () => {
+ expect(() => parseTypeScriptPlaygroundUrl(
+ "https://example.com/play/#code/abc123"
+ )).toThrow("Invalid TypeScript playground URL");
+ });
+
+ it("throws error for TypeScript playground URL without code", () => {
+ expect(() => parseTypeScriptPlaygroundUrl(
+ "https://www.typescriptlang.org/play/"
+ )).toThrow("No code found in TypeScript playground URL");
+ });
+});
diff --git a/src/library/utils/urlParsers.ts b/src/library/utils/urlParsers.ts
index 34f203b..3910dd2 100644
--- a/src/library/utils/urlParsers.ts
+++ b/src/library/utils/urlParsers.ts
@@ -36,3 +36,46 @@ export function parseGithubIssueLink(url: string): { owner: string, repo: string
throw new Error("Invalid issue link URL");
}
}
+
+export type TypeScriptPlaygroundUrlInfo = {
+ code: string;
+ startLine?: number;
+ startColumn?: number;
+ endLine?: number;
+ endColumn?: number;
+};
+
+export function parseTypeScriptPlaygroundUrl(playgroundUrl: string): TypeScriptPlaygroundUrlInfo {
+ // TypeScript playground URL format:
+ // https://www.typescriptlang.org/play/?ssl=44&ssc=1&pln=8&pc=1#code/{compressed_code}
+
+ const url = new URL(playgroundUrl);
+
+ // Validate it's a TypeScript playground URL
+ if ((url.hostname !== 'www.typescriptlang.org' && url.hostname !== 'typescriptlang.org') || !url.pathname.includes('/play')) {
+ throw new Error("Invalid TypeScript playground URL");
+ }
+
+ // Extract code from hash
+ const codeMatch = url.hash.match(/#code\/(.*)/);
+ if (!codeMatch) {
+ throw new Error("No code found in TypeScript playground URL");
+ }
+
+ const compressedCode = codeMatch[1];
+
+ // Parse query parameters for line/column information
+ const params = new URLSearchParams(url.search);
+ const ssl = params.get('ssl'); // start line
+ const ssc = params.get('ssc'); // start column
+ const pln = params.get('pln'); // panel line (end line)
+ const pc = params.get('pc'); // panel column (end column)
+
+ return {
+ code: compressedCode,
+ startLine: ssl ? parseInt(ssl, 10) : undefined,
+ startColumn: ssc ? parseInt(ssc, 10) : undefined,
+ endLine: pln ? parseInt(pln, 10) : undefined,
+ endColumn: pc ? parseInt(pc, 10) : undefined,
+ };
+}