@shiinasaku/github-card 4.2.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ export function kFormat(num: number): string {
16
16
  */
17
17
  export function measureText(text: string, size: number, weight = 400): number {
18
18
  if (!text) return 0;
19
- const factor = size / 16;
19
+ const factor = size;
20
20
  const weightFactor = weight >= 700 ? 1.08 : weight >= 600 ? 1.04 : 1;
21
21
  let width = 0;
22
22
  for (const ch of text) {
@@ -45,6 +45,7 @@ export function measureText(text: string, size: number, weight = 400): number {
45
45
 
46
46
  export function escapeXml(str: string): string {
47
47
  return str
48
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff\ud800-\udfff]/gu, "")
48
49
  .replace(/&/g, "&")
49
50
  .replace(/</g, "&lt;")
50
51
  .replace(/>/g, "&gt;")
@@ -53,28 +54,29 @@ export function escapeXml(str: string): string {
53
54
  }
54
55
 
55
56
  export function wrapText(input: string, maxLen: number, maxLines: number): string[] {
56
- const words = input.split(/\s+/).filter(Boolean);
57
+ if (maxLen < 1 || maxLines < 1) return [];
58
+ let remaining = Array.from(input.trim().replace(/\s+/g, " "));
57
59
  const lines: string[] = [];
58
- let current = "";
59
60
 
60
- for (const word of words) {
61
- const next = current ? `${current} ${word}` : word;
62
- if (next.length <= maxLen) {
63
- current = next;
64
- continue;
61
+ while (remaining.length && lines.length < maxLines) {
62
+ if (remaining.length <= maxLen) {
63
+ lines.push(remaining.join(""));
64
+ break;
65
65
  }
66
- if (current) lines.push(current);
67
- if (lines.length === maxLines) break;
68
- current = word.length > maxLen ? word.slice(0, maxLen) : word;
69
- }
70
- if (current && lines.length < maxLines) lines.push(current);
71
-
72
- // Truncated? Mark the last line with an ellipsis.
73
- const consumed = lines.join(" ").length;
74
- if (lines.length > 0 && consumed < input.trim().length) {
75
- const last = lines[lines.length - 1]!;
76
- lines[lines.length - 1] =
77
- (last.length >= maxLen ? last.slice(0, Math.max(0, maxLen - 1)) : last) + "…";
66
+ if (lines.length === maxLines - 1) {
67
+ lines.push(
68
+ remaining
69
+ .slice(0, maxLen - 1)
70
+ .join("")
71
+ .trimEnd() + "…",
72
+ );
73
+ break;
74
+ }
75
+ const space = remaining.lastIndexOf(" ", maxLen);
76
+ const end = space > 0 ? space : maxLen;
77
+ lines.push(remaining.slice(0, end).join(""));
78
+ remaining = remaining.slice(end);
79
+ if (remaining[0] === " ") remaining.shift();
78
80
  }
79
81
  return lines;
80
82
  }
@@ -1,9 +1,5 @@
1
1
  export class GitHubError extends Error {
2
- constructor(
3
- message: string,
4
- public httpStatus?: number,
5
- public errorCode?: string,
6
- ) {
2
+ constructor(message: string) {
7
3
  super(message);
8
4
  this.name = "GitHubError";
9
5
  }
@@ -11,93 +7,48 @@ export class GitHubError extends Error {
11
7
 
12
8
  export class NotFoundError extends GitHubError {
13
9
  constructor(message = "Resource not found") {
14
- super(message, 404, "NOT_FOUND");
10
+ super(message);
15
11
  this.name = "NotFoundError";
16
12
  }
17
13
  }
18
14
 
19
15
  export class RateLimitError extends GitHubError {
20
- constructor(
21
- message = "GitHub API rate limit exceeded",
22
- public resetAt?: Date,
23
- ) {
24
- super(message, 429, "RATE_LIMITED");
16
+ constructor(message = "GitHub API rate limit exceeded") {
17
+ super(message);
25
18
  this.name = "RateLimitError";
26
19
  }
27
20
  }
28
21
 
29
22
  export class AuthError extends GitHubError {
30
23
  constructor(message = "GitHub authentication failed") {
31
- super(message, 401, "UNAUTHENTICATED");
24
+ super(message);
32
25
  this.name = "AuthError";
33
26
  }
34
27
  }
35
28
 
36
- export class TimeoutError extends GitHubError {
37
- constructor(message = "GitHub API request timed out") {
38
- super(message, 408, "TIMEOUT");
29
+ class TimeoutError extends GitHubError {
30
+ constructor() {
31
+ super("GitHub API request timed out");
39
32
  this.name = "TimeoutError";
40
33
  }
41
34
  }
42
35
 
43
- export interface GitHubClientOptions {
44
- token?: string;
45
- userAgent?: string;
46
- timeoutMs?: number;
47
- baseUrl?: string;
48
- }
49
-
50
- export interface RequestOptions extends Omit<RequestInit, "body"> {
51
- variables?: Record<string, unknown>;
52
- timeoutMs?: number;
53
- }
54
-
55
- /**
56
- * A next-generation, generic GitHub GraphQL client.
57
- * Designed to be robust, strongly typed, and publishable.
58
- */
59
36
  export class GitHubClient {
60
- private readonly token: string;
61
- private readonly userAgent: string;
62
- private readonly timeoutMs: number;
63
- private readonly baseUrl: string;
64
-
65
- constructor(options: GitHubClientOptions = {}) {
66
- const token =
67
- options.token || (typeof process !== "undefined" ? process.env?.GITHUB_TOKEN : "") || "";
68
- if (!token) {
69
- console.warn(
70
- "GitHubClient initialized without a token. Requests requiring authentication will fail.",
71
- );
72
- }
37
+ constructor(private readonly options: { token: string; userAgent: string }) {}
73
38
 
74
- this.token = token;
75
- this.userAgent = options.userAgent || "nextgen-github-client/1.0";
76
- this.timeoutMs = options.timeoutMs || 10000;
77
- this.baseUrl = options.baseUrl || "https://api.github.com/graphql";
78
- }
79
-
80
- /**
81
- * Classifies fetch or API errors into distinct custom error classes.
82
- */
83
- private classifyError(message: string, status: number, headers?: Headers): never {
39
+ private classifyError(message: string, status: number): never {
84
40
  const lower = message.toLowerCase();
85
-
86
41
  if (status === 404 || lower.includes("not resolve to a user") || lower.includes("not found")) {
87
42
  throw new NotFoundError(message);
88
43
  }
89
-
90
44
  if (
91
45
  status === 429 ||
92
46
  status === 403 ||
93
47
  lower.includes("rate limit") ||
94
48
  lower.includes("abuse")
95
49
  ) {
96
- const resetHeader = headers?.get("x-ratelimit-reset");
97
- const resetAt = resetHeader ? new Date(parseInt(resetHeader, 10) * 1000) : undefined;
98
- throw new RateLimitError(message, resetAt);
50
+ throw new RateLimitError(message);
99
51
  }
100
-
101
52
  if (
102
53
  status === 401 ||
103
54
  lower.includes("bad credentials") ||
@@ -105,94 +56,48 @@ export class GitHubClient {
105
56
  ) {
106
57
  throw new AuthError(message);
107
58
  }
108
-
109
- throw new GitHubError(message || `GitHub API error (${status})`, status);
59
+ throw new GitHubError(message || `GitHub API error (${status})`);
110
60
  }
111
61
 
112
- /**
113
- * Executes a GraphQL query against the GitHub API.
114
- *
115
- * @param query The GraphQL query string.
116
- * @param options Request options including variables and fetch options.
117
- * @returns Strongly typed response data.
118
- */
119
- public async request<T = unknown>(query: string, options: RequestOptions = {}): Promise<T> {
120
- const { variables, timeoutMs = this.timeoutMs, signal: userSignal, ...fetchOptions } = options;
121
-
122
- const headers = new Headers(fetchOptions.headers);
123
- if (this.token && !headers.has("Authorization")) {
124
- headers.set("Authorization", `bearer ${this.token}`);
125
- }
126
- if (!headers.has("Content-Type")) {
127
- headers.set("Content-Type", "application/json");
128
- }
129
- if (!headers.has("User-Agent")) {
130
- headers.set("User-Agent", this.userAgent);
131
- }
132
-
133
- const abortController = new AbortController();
134
- const timeoutId =
135
- timeoutMs > 0
136
- ? setTimeout(() => abortController.abort(new TimeoutError()), timeoutMs)
137
- : undefined;
138
-
139
- // Link user-provided signal if present
140
- const onAbort = userSignal ? () => abortController.abort(userSignal.reason) : undefined;
141
- if (userSignal) {
142
- userSignal.addEventListener("abort", onAbort!);
143
- if (userSignal.aborted) abortController.abort(userSignal.reason);
144
- }
145
-
146
- let response: Response;
147
-
62
+ async request<T>(
63
+ query: string,
64
+ { variables, timeoutMs }: { variables: Record<string, unknown>; timeoutMs: number },
65
+ ): Promise<T> {
148
66
  try {
149
- response = await fetch(this.baseUrl, {
150
- ...fetchOptions,
67
+ const response = await fetch("https://api.github.com/graphql", {
151
68
  method: "POST",
152
- headers,
69
+ headers: {
70
+ Authorization: `bearer ${this.options.token}`,
71
+ "Content-Type": "application/json",
72
+ "User-Agent": this.options.userAgent,
73
+ },
153
74
  body: JSON.stringify({ query, variables }),
154
- signal: abortController.signal,
75
+ signal: AbortSignal.timeout(timeoutMs),
155
76
  });
156
- } catch (error: unknown) {
157
- const message = error instanceof Error ? error.message : String(error);
158
- const name = error instanceof Error ? error.name : "";
159
- if (name === "AbortError" || error instanceof TimeoutError) {
160
- throw new TimeoutError(message || "Request timed out");
161
- }
162
- throw new GitHubError(`Network error: ${message || "Unknown error"}`);
163
- } finally {
164
- if (timeoutId) clearTimeout(timeoutId);
165
- if (userSignal) {
166
- userSignal.removeEventListener("abort", onAbort!);
77
+ if (!response.ok) {
78
+ const message = await response.text();
79
+ this.classifyError(message || response.statusText, response.status);
167
80
  }
168
- }
169
-
170
- if (!response.ok) {
171
- const errorText = await response.text().catch(() => "");
172
- this.classifyError(errorText || response.statusText, response.status, response.headers);
173
- }
174
-
175
- const body = (await response.json().catch(() => ({}))) as {
176
- data?: T;
177
- errors?: { message: string }[];
178
- };
179
81
 
180
- if (body.errors && body.errors.length > 0) {
181
- const firstError = body.errors[0];
182
- const message = firstError?.message ?? "GraphQL error";
183
- this.classifyError(message, response.status, response.headers);
184
- }
185
-
186
- if (!body.data) {
82
+ const body = (await response.json()) as { data?: T; errors?: { message: string }[] };
83
+ if (body.errors?.length) {
84
+ this.classifyError(body.errors[0]?.message || "GraphQL error", response.status);
85
+ }
86
+ if (body.data == null) {
87
+ throw new GitHubError("Invalid response: No data returned from GraphQL endpoint");
88
+ }
89
+ return body.data;
90
+ } catch (error) {
91
+ if (error instanceof GitHubError) throw error;
92
+ if (
93
+ error instanceof Error &&
94
+ (error.name === "AbortError" || error.name === "TimeoutError")
95
+ ) {
96
+ throw new TimeoutError();
97
+ }
187
98
  throw new GitHubError(
188
- "Invalid response: No data returned from GraphQL endpoint",
189
- response.status,
99
+ `Network error: ${error instanceof Error ? error.message : String(error)}`,
190
100
  );
191
101
  }
192
-
193
- return body.data as T;
194
102
  }
195
103
  }
196
-
197
- // Export a default singleton instance for convenience
198
- export const githubClient = new GitHubClient();
@@ -1,3 +1,5 @@
1
+ import { escapeXml } from "./format";
2
+
1
3
  export const icons = {
2
4
  star: `<path d="M10.92 2.868a1.25 1.25 0 0 1 2.16 0l2.795 4.798l5.428 1.176a1.25 1.25 0 0 1 .667 2.054l-3.7 4.141l.56 5.525a1.25 1.25 0 0 1-1.748 1.27L12 19.592l-5.082 2.24a1.25 1.25 0 0 1-1.748-1.27l.56-5.525l-3.7-4.14a1.25 1.25 0 0 1 .667-2.055l5.428-1.176z"/>`,
3
5
  commit: `<path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z"/>`,
@@ -26,7 +28,9 @@ export function icon(name: IconName, color: string, size = 16): string {
26
28
  if (!path) return "";
27
29
  const meta = iconMeta[name];
28
30
  const viewBox = meta?.viewBox ?? "0 0 16 16";
29
- const colorStr = color.startsWith("#") ? color : `#${color}`;
31
+ const colorStr = escapeXml(
32
+ /^(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i.test(color) ? `#${color}` : color,
33
+ );
30
34
  const attrs =
31
35
  meta?.kind === "stroke"
32
36
  ? `fill="none" stroke="${colorStr}" stroke-width="${meta.strokeWidth ?? "1.8"}" stroke-linecap="round" stroke-linejoin="round"`
@@ -1,6 +1,5 @@
1
- export { resolveColors, themes, type Theme } from "./themes";
1
+ export { resolveColors, themes } from "./themes";
2
2
  export { kFormat, escapeXml, wrapText, measureText } from "./format";
3
- export { getLangColor } from "./languages";
4
- export { icons, icon } from "./icons";
5
- export { FONT_FACE, FONT_FAMILY } from "./font";
6
- export * from "./colors";
3
+ export { icon } from "./icons";
4
+ export { FONT_FAMILY } from "./font";
5
+ export { resolveTw } from "./colors";
@@ -1,4 +1,4 @@
1
- export const langColors: Record<string, string> = {
1
+ const langColors: Record<string, string> = {
2
2
  "1C Enterprise": "#814CCC",
3
3
  "2-Dimensional Array": "#38761D",
4
4
  "4D": "#004289",
package/src/env.d.ts DELETED
@@ -1,4 +0,0 @@
1
- declare module "*.css" {
2
- const content: string;
3
- export default content;
4
- }
package/src/input.css DELETED
@@ -1 +0,0 @@
1
- @import "tailwindcss";