diff --git a/README.md b/README.md
index ab1e933..96dfd20 100644
--- a/README.md
+++ b/README.md
@@ -57,19 +57,20 @@ Generate a top languages chart for your GitHub profile that you can embed in a R
### Customization Options
Append these query parameters to the URL to customize the look and data of your chart:
-| Parameter | Type | Description | Default | Example |
-| :--- | :--- | :--- | :--- | :--- |
-| `theme` | String | Sets the colour scheme. Available options: `default`, `light`, `dark`. | `default` | `?theme=dark` |
-| `title` | String | Sets a custom title for the chart. | `Top Languages` | `?title=My%20Code%20Stack` |
-| `hide_title` | Boolean | Hides the chart title completely. | `false` | `?hide_title=true` |
-| `count` | Number | Sets the maximum number of languages to display. Max is **16**. | `8` | `?count=10` |
-| `width` | Number | Sets the width of the SVG in pixels. | `400` | `?width=500` |
-| `height` | Number | Sets the height of the SVG in pixels. | `300` | `?height=350` |
-| `bg` | String | Sets the chart background colour. Accepts hex (`ffffff`) or theme names (`dark`, `light`). | `default` | `?bg=dark` |
-| `text` | String | Sets the chart text colour. Accepts hex (`ffffff`). | `#000000` | `?text=ffffff` |
-| `c1`-`c16` | String | Sets individual colours for languages 1-16. Accepts hex codes. | Auto-assigned | `?c1=ff0000&c2=00ff00` |
-| `test` | Boolean | Uses samples data instead of fetching from GitHub API. | `false` | `?test=true` |
-| `stroke` | Boolean | Adds an outline stroke to chart segments. | `false` | `?stroke=true` |
+| Parameter | Type | Description | Default | Example |
+| :--- | :--- | :--- | :--- | :--- |
+| `theme` | String | Sets the colour scheme. Available options: `default`, `light`, `dark`. | `default` | `?theme=dark` |
+| `type` | String | Sets the chart type. Available options: `donut`, `pie`. | `donut` | `?type=pie` |
+| `title` | String | Sets a custom title for the chart. | `Top Languages` | `?title=My%20Code%20Stack` |
+| `hide_title` | Boolean | Hides the chart title completely. | `false` | `?hide_title=true` |
+| `text` | String | Sets the chart text colour. Accepts hex (`ffffff`). | `#000000` | `?text=ffffff` |
+| `bg` | String | Sets the chart background colour. Accepts hex (`ffffff`) or theme names (`dark`). | `default` | `?bg=dark` |
+| `c1`-`c16` | String | Sets individual colours for languages 1-16. Accepts hex codes. | Auto-assigned | `?c1=ff0000&c2=00ff00` |
+| `count` | Number | Sets the maximum number of languages to display. Max is **16**. | `8` | `?count=10` |
+| `width` | Number | Sets the width of the SVG in pixels. | `400` | `?width=500` |
+| `height` | Number | Sets the height of the SVG in pixels. | `300` | `?height=350` |
+| `stroke` | Boolean | Adds an outline stroke to chart segments. | `false` | `?stroke=true` |
+| `test` | Boolean | Uses samples data instead of fetching from GitHub API. | `false` | `?test=true` |
#### Example URL
To get 10 languages, a dark theme, and a custom title:
@@ -77,9 +78,6 @@ To get 10 languages, a dark theme, and a custom title:

```
-
-
-
## Deployment & Configuration
### Prerequisites
@@ -96,8 +94,9 @@ npm install
### Configuration
Copy `.env.example` to `.env`, and update the variables.
-- `GITHUB_USERNAME`: User to fetch repositories and statistics.
-- `IGNORED_REPOS`: Optional repos you don't want counted in your chart.
+- `GITHUB_USERNAMES`: Comma-separated GitHub usernames to fetch repositories from.
+- `GITHUB_ORGS`: Optional comma-separated GitHub organization names to include.
+- `IGNORED_REPOS`: Optional comma-separated repo names to exclude from the chart.
### Running Locally
```bash
@@ -106,12 +105,10 @@ vercel dev
```
### Deployment
-- **Vercel**: Simply import your repo and deploy. The default endpoint is /api/languages
-
-
-
+[](https://vercel.com/new/clone?repository-url=https://github.com/masonlet/github-top-languages&env=GITHUB_USERNAMES,IGNORED_REPOS&envDescription[GITHUB_USERNAMES]=Comma-separated%20GitHub%20usernames&envDescription[IGNORED_REPOS]=Optional%20comma-separated%20repos%20to%20exclude)
+> The default endpoint is /api/languages
## License
MIT License - see [LICENSE](./LICENSE) for details.
diff --git a/api/languages/index.ts b/api/languages/index.ts
index a62eaf0..97dcb56 100644
--- a/api/languages/index.ts
+++ b/api/languages/index.ts
@@ -1,6 +1,6 @@
import type { VercelRequest, VercelResponse } from "@vercel/node";
import { parseQueryParams, type QueryParams } from "../../src/utils/params.js";
-import { fetchLanguageData, processLanguageData } from "../../src/api/github.js";
+import { fetchLanguageData, processLanguageData } from "../../src/services/github.js";
import { generateChartData } from "../../src/render/chart.js";
import { renderSvg } from "../../src/render/svg.js";
import { renderError } from "../../src/render/error.js";
diff --git a/src/render/error.ts b/src/render/error.ts
index 54f969a..55a3459 100644
--- a/src/render/error.ts
+++ b/src/render/error.ts
@@ -1,6 +1,7 @@
import type { Theme } from "../types.js";
import { THEMES } from "../constants/themes.js";
import { ERROR_STYLES } from "../constants/styles.js"
+import { sanitize } from "../utils/sanitize.js";
export function renderError(
message: string,
@@ -8,13 +9,13 @@ export function renderError(
height: number,
selectedTheme?: Theme
): string {
- const background = selectedTheme?.bg || THEMES.default.bg;
+ const background = selectedTheme?.bg || THEMES.default.bg;
return `
- `;
+ `.trim();
}
diff --git a/src/render/svg.ts b/src/render/svg.ts
index 8f30107..9a78ad8 100644
--- a/src/render/svg.ts
+++ b/src/render/svg.ts
@@ -27,5 +27,5 @@ export function renderSvg(
${segments}
${legend}
- `;
+ `.trim();
}
diff --git a/src/api/github.ts b/src/services/github.ts
similarity index 100%
rename from src/api/github.ts
rename to src/services/github.ts
diff --git a/src/utils/params.ts b/src/utils/params.ts
index e9be187..c3bb486 100644
--- a/src/utils/params.ts
+++ b/src/utils/params.ts
@@ -14,14 +14,16 @@ const parseIntSafe = (
return Number.isNaN(parsed) ? fallback : parsed;
}
+const normalizeHex = (val: string) => `#${val.replace(/^#/, '')}`;
+
export function parseQueryParams(query: QueryParams) {
const baseTheme = THEMES[query["theme"] as keyof typeof THEMES] ?? THEMES.default;
const count = parseIntSafe(query["count"], DEFAULT_CONFIG.COUNT);
- const customColours = [...baseTheme.colours] as string[];
+ const customColours: string[] = [...baseTheme.colours];
for (let i = 1; i <= MAX_COUNT; i++) {
const colourVal = query[`c${i}`];
- if(colourVal) customColours[i - 1] = `#${colourVal.replace(/^#/, '')}` as string;
+ if(colourVal) customColours[i - 1] = normalizeHex(colourVal);
}
const typeParam = query["type"] as ChartType | undefined;
@@ -34,8 +36,8 @@ export function parseQueryParams(query: QueryParams) {
height: parseIntSafe(query["height"], DEFAULT_CONFIG.HEIGHT),
count: Math.min(Math.max(count, 1), MAX_COUNT),
selectedTheme: {
- bg: THEMES[query["bg"] as keyof typeof THEMES]?.bg ?? query["bg"] ?? baseTheme.bg,
- text: query["text"] ?? baseTheme.text,
+ bg: THEMES[query["bg"] as keyof typeof THEMES]?.bg ?? (query["bg"] ? normalizeHex(query["bg"]) : baseTheme.bg),
+ text: query["text"] ? normalizeHex(query["text"]) : baseTheme.text,
colours: customColours
},
stroke: query["stroke"] === "true",
diff --git a/tests/api/languages/index.test.ts b/tests/api/languages/index.test.ts
index 83e9ce4..2ecc208 100644
--- a/tests/api/languages/index.test.ts
+++ b/tests/api/languages/index.test.ts
@@ -3,13 +3,13 @@ import type { VercelRequest, VercelResponse } from "@vercel/node";
import type { ChartResult } from "../../../src/types.js";
import handler from "../../../api/languages/index.js";
import { parseQueryParams } from "../../../src/utils/params.js";
-import { fetchLanguageData, processLanguageData } from "../../../src/api/github.js";
+import { fetchLanguageData, processLanguageData } from "../../../src/services/github.js";
import { generateChartData } from "../../../src/render/chart.js";
import { renderSvg } from "../../../src/render/svg.js";
import { renderError } from "../../../src/render/error.js";
vi.mock("../../../src/utils/params.js");
-vi.mock("../../../src/api/github.js");
+vi.mock("../../../src/services/github.js");
vi.mock("../../../src/render/chart.js");
vi.mock("../../../src/render/svg.js");
vi.mock("../../../src/render/error.js");
diff --git a/tests/api/github.test.ts b/tests/services/github.test.ts
similarity index 99%
rename from tests/api/github.test.ts
rename to tests/services/github.test.ts
index c2a8613..8f49e16 100644
--- a/tests/api/github.test.ts
+++ b/tests/services/github.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { fetchLanguageData, processLanguageData, resetCache } from "../../src/api/github.js";
+import { fetchLanguageData, processLanguageData, resetCache } from "../../src/services/github.js";
const repos = [
{ name: "repo1", fork: false, full_name: "user/repo1" },