Skip to content
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
5 changes: 5 additions & 0 deletions packages/core/src/api/api-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** What every api handler returns: a rendered card, or a rendered error. */
export interface ApiResult {
status: "success" | "error - permanent" | "error - temporary";
content: string;
}
51 changes: 39 additions & 12 deletions packages/core/src/api/gist.js → packages/core/src/api/gist.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { renderGistCard } from "../cards/gist.js";
import type { ColorParams } from "../common/color.js";
import { findInvalidColorParam, pickColorParams } from "../common/color.js";
import {
MissingParamError,
Expand All @@ -7,21 +8,44 @@ import {
import { parseBoolean } from "../common/ops.js";
import { renderError } from "../common/render.js";
import { fetchGist } from "../fetchers/gist.js";
import { isLocaleAvailable } from "../translations.js";

// @ts-ignore
import type { ApiResult } from "./api-result.js";

/** Query params the gist endpoint accepts, on top of the shared color params. */
interface GistApiQuery extends ColorParams {
id?: string;
border_radius?: string;
show_owner?: string;
browser_rendering?: string;
hide_border?: string;
}

/** Characters a gist ID may contain. */
const SAFE_PATTERN = /^[-\w/.,]+$/;

/**
* Render the gist card for a set of query params.
*
* @param query Raw query params.
* @param query.id GitHub gist ID.
* @param query.border_radius Card border radius.
* @param query.show_owner Whether to show the gist owner.
* @param query.browser_rendering Whether the browser wraps the description text.
* @param query.hide_border Whether to hide the card border.
* @param pat Optional PAT override.
* @returns The rendered card, or a rendered error.
*/
export default async (
{
id,
locale,
border_radius,
show_owner,
browser_rendering,
hide_border,
...remainingParams
},
pat = null,
) => {
}: GistApiQuery,
pat: string | null = null,
): Promise<ApiResult> => {
const colorParams = pickColorParams(remainingParams);

const invalidColorInput = findInvalidColorParam(colorParams);
Expand All @@ -35,19 +59,23 @@ export default async (
};
}

if (locale && !isLocaleAvailable(locale)) {
const borderRadiusParsed =
border_radius === undefined ? undefined : parseFloat(border_radius);
if (
borderRadiusParsed !== undefined &&
!Number.isFinite(borderRadiusParsed)
) {
return {
status: "error - permanent",
content: renderError({
message: "Something went wrong",
secondaryMessage: "Language not found",
secondaryMessage: 'Invalid number input for parameter "border_radius"',
renderOptions: colorParams,
}),
};
}

const safePattern = /^[-\w/.,]+$/;
if (id && !safePattern.test(id)) {
if (id && !SAFE_PATTERN.test(id)) {
return {
status: "error - permanent",
content: renderError({
Expand All @@ -65,8 +93,7 @@ export default async (
status: "success",
content: renderGistCard(gistData, {
...colorParams,
border_radius,
locale: locale ? locale.toLowerCase() : null,
border_radius: borderRadiusParsed,
show_owner: parseBoolean(show_owner),
browser_rendering: parseBoolean(browser_rendering),
hide_border: parseBoolean(hide_border),
Expand Down
13 changes: 0 additions & 13 deletions packages/core/src/cards/common-options.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/core/src/cards/gist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "../common/render.js";
import type { GistData } from "../fetchers/types.js";

import type { CommonOptions } from "./common-options.js";
import type { CardOptions, CommonCardOptions } from "./options.js";

const ICON_SIZE = 16;
const CARD_DEFAULT_WIDTH = 400;
Expand All @@ -27,7 +27,7 @@ const DESCRIPTION_FONT_SIZE = 13;
const DESCRIPTION_LINE_HEIGHT_PX = 16;
const DESCRIPTION_MAX_LINES = 10;

interface GistCardOptions extends CommonOptions {
interface GistCardOptions extends CommonCardOptions {
show_owner: boolean;
browser_rendering: boolean;
}
Expand All @@ -41,7 +41,7 @@ interface GistCardOptions extends CommonOptions {
*/
const renderGistCard = (
gistData: GistData,
options: Partial<GistCardOptions> = {},
options: CardOptions<GistCardOptions> = {},
): string => {
const { name, nameWithOwner, description, language, starsCount, forksCount } =
gistData;
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/cards/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ColorParams } from "../common/color.js";

/**
* Options every card accepts.
* Cards spread these into `getLightDarkColors`, hence {@link ColorParams}.
*/
export interface CommonCardOptions extends ColorParams {
title_color: string;
icon_color: string;
text_color: string;
bg_color: string;
border_radius: number;
border_color: string;
hide_border: boolean;
}

/** `Partial<T>` that also accepts an explicit `undefined`, as api handlers forward. */
export type CardOptions<T> = { [K in keyof T]?: T[K] | undefined };
5 changes: 3 additions & 2 deletions packages/core/src/cards/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import type { RepositoryData } from "../fetchers/types.js";
import { repoCardLocales } from "../translations.js";

import type { CommonOptions } from "./common-options.js";
import type { CommonCardOptions } from "./options.js";

const ICON_SIZE = 16;
const CARD_DEFAULT_WIDTH = 400;
Expand All @@ -27,7 +27,8 @@ const DESCRIPTION_FONT_SIZE = 13;
const DESCRIPTION_LINE_HEIGHT_PX = 16;
const DESCRIPTION_MAX_LINES = 3;

interface RepoCardOptions extends CommonOptions {
interface RepoCardOptions extends CommonCardOptions {
locale: string;
show_owner: boolean;
browser_rendering: boolean;
description_lines_count: number;
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/cards/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createTextNode, flexLayout, measureText } from "../common/render.js";
import type { StatsData } from "../fetchers/types.js";
import { statCardLocales, wakatimeCardLocales } from "../translations.js";

import type { CommonOptions } from "./common-options.js";
import type { CommonCardOptions } from "./options.js";

const CARD_MIN_WIDTH = 287;
const CARD_DEFAULT_WIDTH = 287;
Expand All @@ -19,7 +19,8 @@ const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;

type RankIcon = "default" | "github" | "percentile";

interface StatCardOptions extends CommonOptions {
interface StatCardOptions extends CommonCardOptions {
locale: string;
hide: Array<string>;
show_icons: boolean;
hide_title: boolean;
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/cards/top-languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import type { Lang, TopLangData } from "../fetchers/types.js";
import { langCardLocales } from "../translations.js";

import type { CommonOptions } from "./common-options.js";
import type { CommonCardOptions } from "./options.js";

const DEFAULT_CARD_WIDTH = 300;
const MIN_CARD_WIDTH = 280;
Expand All @@ -29,7 +29,8 @@ const DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT = 6;

type TopLangLayout = "compact" | "normal" | "donut" | "donut-vertical" | "pie";

interface TopLangOptions extends CommonOptions {
interface TopLangOptions extends CommonCardOptions {
locale: string;
hide_title: boolean;
card_width: number;
hide: Array<string>;
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/cards/wakatime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createProgressNode, flexLayout } from "../common/render.js";
import type { WakaTimeData, WakaTimeLang } from "../fetchers/types.js";
import { wakatimeCardLocales } from "../translations.js";

import type { CommonOptions } from "./common-options.js";
import type { CommonCardOptions } from "./options.js";

const DEFAULT_CARD_WIDTH = 495;
const MIN_CARD_WIDTH = 250;
Expand All @@ -22,7 +22,8 @@ const TOTAL_TEXT_WIDTH = 275;
type WakaTimeLayout = "compact" | "normal";
type DisplayFormat = "time" | "percent";

interface WakaTimeOptions extends CommonOptions {
interface WakaTimeOptions extends CommonCardOptions {
locale: string;
hide_title: boolean;
hide: Array<string>;
card_width: number;
Expand Down
13 changes: 5 additions & 8 deletions packages/core/src/common/color.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { themes } from "../themes/index.js";
import type { ThemeName } from "../themes/index.js";
import { isThemeName, themes } from "../themes/index.js";

/** Matches a 3-, 4-, 6-, or 8-digit hex color with no leading `#`. */
const HEX_COLOR =
Expand Down Expand Up @@ -172,11 +171,7 @@ const getCardColors = ({
theme,
}: ColorInput): CardColors => {
const defaultTheme = themes.default;
const isThemeProvided = theme !== undefined && theme in themes;

const selectedTheme = isThemeProvided
? themes[theme as ThemeName]
: defaultTheme;
const selectedTheme = isThemeName(theme) ? themes[theme] : defaultTheme;

const defaultBorderColor =
"border_color" in selectedTheme
Expand Down Expand Up @@ -275,7 +270,7 @@ const extractLightDarkColors = (
* @returns `{ lightColors, darkColors }`, resolved colors for both light and dark mode
*/
const getLightDarkColors = (
params: ColorInput & LightDarkColorParams,
params: ColorParams,
): { lightColors: CardColors; darkColors: CardColors | null } => {
const lightOverrides = extractLightDarkColors(params, "_light");
const darkOverrides = extractLightDarkColors(params, "_dark");
Expand Down Expand Up @@ -343,6 +338,8 @@ const findInvalidColorParam = (params: ColorParams): string | null =>
),
);

export type { ColorParams };

export {
getCardColors,
getLightDarkColors,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/common/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { CustomError } from "./error.js";
/**
* Returns boolean if value is either "true" or "false" else the value as it is.
*
* @param value The value to parse.
* @param value The value to parse; `undefined` when the param was not sent.
* @returns The parsed value.
*/
const parseBoolean = (value: string | boolean): boolean | undefined => {
const parseBoolean = (
value: string | boolean | undefined,
): boolean | undefined => {
if (typeof value === "boolean") {
return value;
}
Expand Down
15 changes: 8 additions & 7 deletions packages/core/src/common/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,14 +394,15 @@ const renderError = ({
renderOptions = {},
}: {
message: string;
secondaryMessage?: string;
secondaryMessage?: string | undefined;
// `| undefined`: api handlers forward absent query params
renderOptions?: {
title_color?: string;
text_color?: string;
bg_color?: string;
border_color?: string;
theme?: string;
show_repo_link?: boolean;
title_color?: string | undefined;
text_color?: string | undefined;
bg_color?: string | undefined;
border_color?: string | undefined;
theme?: string | undefined;
show_repo_link?: boolean | undefined;
};
}): string => {
const {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/fetchers/gist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const calculatePrimaryLanguage = (
* @returns Gist data.
*/
const fetchGist = async (
id: string,
id: string | undefined,
pat: string | null = null,
): Promise<GistData> => {
if (!id) {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/themes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,13 @@ export const themes = {
* Name of one of the {@link themes}.
*/
export type ThemeName = keyof typeof themes;

/**
* Checks whether a value is one of the {@link themes}
*/
export const isThemeName = (value: unknown): value is ThemeName => {
if (typeof value !== "string") {
return false;
}
return value in themes;
};
1 change: 0 additions & 1 deletion packages/core/tests/ops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe("Test ops.js", () => {
expect(parseBoolean("1")).toBe(undefined);
expect(parseBoolean("0")).toBe(undefined);
expect(parseBoolean("")).toBe(undefined);
// @ts-expect-error testing invalid input
expect(parseBoolean(undefined)).toBe(undefined);
});

Expand Down
24 changes: 17 additions & 7 deletions packages/core/tests/renderGistCard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe("test renderGistCard", () => {
it("should render with all the themes", () => {
Object.entries(themes).forEach(([name, themeData]) => {
document.body.innerHTML = renderGistCard(data, {
theme: name as keyof typeof themes,
theme: name,
});

const styleTag = document.querySelector("style");
Expand Down Expand Up @@ -269,16 +269,26 @@ describe("test renderGistCard", () => {

describe("test gist API", () => {
it("should return permanent error for invalid color input", async () => {
const result = await gistApi(
// api handler accepts a partial options object at runtime
{ id: "abc123", title_color: "not-a-color" } as Parameters<
typeof gistApi
>[0],
);
const result = await gistApi({
id: "abc123",
title_color: "not-a-color",
});

expect(result.status).toBe("error - permanent");
expect(result.content).toContain(
`Invalid color input for parameter &#34;title_color&#34;`,
);
});

it.each(["abc", ""])(
"should return permanent error for border_radius %j",
async (border_radius) => {
const result = await gistApi({ id: "abc123", border_radius });

expect(result.status).toBe("error - permanent");
expect(result.content).toContain(
`Invalid number input for parameter &#34;border_radius&#34;`,
);
},
);
});
2 changes: 1 addition & 1 deletion packages/core/tests/renderRepoCard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ describe("Test renderRepoCard", () => {
it("should render with all the themes", () => {
Object.entries(themes).forEach(([name, themeData]) => {
document.body.innerHTML = renderRepoCard(data_repo.repository, {
theme: name as keyof typeof themes,
theme: name,
});

const styleTag = document.querySelector("style");
Expand Down
Loading