@stats-forge/github-stats-forge-core 0.0.1 → 0.1.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.
- package/README.md +0 -10
- package/build/api/api-result.d.ts +6 -7
- package/build/api/api-result.d.ts.map +1 -1
- package/build/api/api-result.js +4 -5
- package/build/api/gist.d.ts +3 -10
- package/build/api/gist.d.ts.map +1 -1
- package/build/api/gist.js +4 -11
- package/build/api/index.d.ts +12 -12
- package/build/api/params.d.ts +11 -8
- package/build/api/params.d.ts.map +1 -1
- package/build/api/params.js +16 -14
- package/build/api/pin.d.ts +3 -19
- package/build/api/pin.d.ts.map +1 -1
- package/build/api/pin.js +6 -22
- package/build/api/stats.d.ts +10 -4
- package/build/api/stats.d.ts.map +1 -1
- package/build/api/stats.js +6 -32
- package/build/api/top-langs.d.ts +12 -6
- package/build/api/top-langs.d.ts.map +1 -1
- package/build/api/top-langs.js +6 -26
- package/build/api/wakatime.d.ts +13 -5
- package/build/api/wakatime.d.ts.map +1 -1
- package/build/api/wakatime.js +4 -21
- package/build/calculateRank.d.ts +0 -9
- package/build/calculateRank.d.ts.map +1 -1
- package/build/calculateRank.js +12 -17
- package/build/cards/gist.d.ts +2 -4
- package/build/cards/gist.d.ts.map +1 -1
- package/build/cards/gist.js +27 -36
- package/build/cards/options.d.ts +1 -1
- package/build/cards/repo.d.ts +2 -4
- package/build/cards/repo.d.ts.map +1 -1
- package/build/cards/repo.js +61 -93
- package/build/cards/stats.d.ts +3 -8
- package/build/cards/stats.d.ts.map +1 -1
- package/build/cards/stats.js +119 -165
- package/build/cards/top-languages.d.ts +5 -31
- package/build/cards/top-languages.d.ts.map +1 -1
- package/build/cards/top-languages.js +176 -384
- package/build/cards/wakatime.d.ts +4 -6
- package/build/cards/wakatime.d.ts.map +1 -1
- package/build/cards/wakatime.js +118 -185
- package/build/common/Card.d.ts +14 -62
- package/build/common/Card.d.ts.map +1 -1
- package/build/common/Card.js +97 -199
- package/build/common/I18n.d.ts +0 -6
- package/build/common/I18n.d.ts.map +1 -1
- package/build/common/I18n.js +3 -6
- package/build/common/color.d.ts +4 -17
- package/build/common/color.d.ts.map +1 -1
- package/build/common/color.js +67 -49
- package/build/common/config.d.ts +4 -14
- package/build/common/config.d.ts.map +1 -1
- package/build/common/config.js +3 -16
- package/build/common/constants.d.ts +4 -2
- package/build/common/constants.d.ts.map +1 -1
- package/build/common/constants.js +3 -1
- package/build/common/date.d.ts +0 -2
- package/build/common/date.d.ts.map +1 -1
- package/build/common/date.js +0 -2
- package/build/common/emojiMap.d.ts +8 -0
- package/build/common/emojiMap.d.ts.map +1 -0
- package/build/common/emojiMap.js +10 -0
- package/build/common/emojiMap.json +1915 -0
- package/build/common/error.d.ts +1 -10
- package/build/common/error.d.ts.map +1 -1
- package/build/common/error.js +1 -10
- package/build/common/fmt.d.ts +3 -15
- package/build/common/fmt.d.ts.map +1 -1
- package/build/common/fmt.js +7 -35
- package/build/common/http.d.ts +2 -7
- package/build/common/http.d.ts.map +1 -1
- package/build/common/http.js +8 -17
- package/build/common/icons.d.ts +18 -20
- package/build/common/icons.d.ts.map +1 -1
- package/build/common/icons.js +103 -67
- package/build/common/languageColors.d.ts +0 -1
- package/build/common/languageColors.d.ts.map +1 -1
- package/build/common/languageColors.js +1 -4
- package/build/common/languageColors.json +18 -2
- package/build/common/ops.d.ts +3 -14
- package/build/common/ops.d.ts.map +1 -1
- package/build/common/ops.js +19 -30
- package/build/common/render.d.ts +28 -82
- package/build/common/render.d.ts.map +1 -1
- package/build/common/render.js +175 -211
- package/build/common/retryer.d.ts +2 -7
- package/build/common/retryer.d.ts.map +1 -1
- package/build/common/retryer.js +4 -11
- package/build/fetchers/gist.d.ts +2 -5
- package/build/fetchers/gist.d.ts.map +1 -1
- package/build/fetchers/gist.js +1 -5
- package/build/fetchers/index.d.ts +12 -12
- package/build/fetchers/repo.d.ts +2 -11
- package/build/fetchers/repo.d.ts.map +1 -1
- package/build/fetchers/repo.js +20 -33
- package/build/fetchers/stats.d.ts +2 -32
- package/build/fetchers/stats.d.ts.map +1 -1
- package/build/fetchers/stats.js +27 -86
- package/build/fetchers/top-languages.d.ts +2 -9
- package/build/fetchers/top-languages.d.ts.map +1 -1
- package/build/fetchers/top-languages.js +15 -31
- package/build/fetchers/types.d.ts +1 -1
- package/build/fetchers/wakatime.d.ts +2 -6
- package/build/fetchers/wakatime.d.ts.map +1 -1
- package/build/fetchers/wakatime.js +1 -5
- package/build/graphql/contributionsDocument.d.ts +3 -3
- package/build/graphql/contributionsDocument.d.ts.map +1 -1
- package/build/graphql/contributionsDocument.js +0 -1
- package/build/graphql/generated/gist.d.ts +1 -1
- package/build/graphql/generated/repo.d.ts +1 -1
- package/build/graphql/generated/stats.d.ts +3 -3
- package/build/graphql/generated/top-languages.d.ts +2 -2
- package/build/graphql/graphqlDocument.d.ts +0 -1
- package/build/graphql/graphqlDocument.d.ts.map +1 -1
- package/build/graphql/graphqlDocument.js +0 -1
- package/build/graphql/reposContributedToDocument.d.ts +4 -5
- package/build/graphql/reposContributedToDocument.d.ts.map +1 -1
- package/build/graphql/reposContributedToDocument.js +0 -2
- package/build/markup/css.d.ts +35 -0
- package/build/markup/css.d.ts.map +1 -0
- package/build/markup/css.js +64 -0
- package/build/markup/element.d.ts +41 -0
- package/build/markup/element.d.ts.map +1 -0
- package/build/markup/element.js +127 -0
- package/build/markup/html.d.ts +13 -0
- package/build/markup/html.d.ts.map +1 -0
- package/build/markup/html.js +27 -0
- package/build/markup/index.d.ts +9 -0
- package/build/markup/index.d.ts.map +1 -0
- package/build/markup/index.js +6 -0
- package/build/markup/serialize.d.ts +16 -0
- package/build/markup/serialize.d.ts.map +1 -0
- package/build/markup/serialize.js +17 -0
- package/build/themes/index.d.ts +332 -333
- package/build/themes/index.d.ts.map +1 -1
- package/build/themes/index.js +1 -1
- package/build/translations.d.ts +2 -6
- package/build/translations.d.ts.map +1 -1
- package/build/translations.js +621 -628
- package/package.json +17 -26
- package/src/api/api-result.ts +10 -9
- package/src/api/gist.ts +11 -18
- package/src/api/index.ts +12 -12
- package/src/api/params.ts +27 -21
- package/src/api/pin.ts +13 -28
- package/src/api/stats.ts +13 -39
- package/src/api/top-langs.ts +13 -33
- package/src/api/wakatime.ts +11 -28
- package/src/calculateRank.ts +12 -23
- package/src/cards/gist.ts +40 -43
- package/src/cards/options.ts +1 -1
- package/src/cards/repo.ts +97 -107
- package/src/cards/stats.ts +165 -184
- package/src/cards/top-languages.ts +297 -413
- package/src/cards/wakatime.ts +193 -221
- package/src/common/Card.ts +165 -217
- package/src/common/I18n.ts +3 -6
- package/src/common/color.ts +80 -57
- package/src/common/config.ts +5 -18
- package/src/common/constants.ts +5 -2
- package/src/common/date.ts +0 -2
- package/src/common/emojiMap.json +1915 -0
- package/src/common/emojiMap.ts +13 -0
- package/src/common/error.ts +2 -11
- package/src/common/fmt.ts +7 -44
- package/src/common/http.ts +9 -15
- package/src/common/icons.ts +117 -42
- package/src/common/languageColors.json +18 -2
- package/src/common/languageColors.ts +1 -4
- package/src/common/ops.ts +26 -34
- package/src/common/render.ts +279 -232
- package/src/common/retryer.ts +8 -15
- package/src/fetchers/gist.ts +8 -12
- package/src/fetchers/index.ts +12 -12
- package/src/fetchers/repo.ts +27 -40
- package/src/fetchers/stats.ts +44 -102
- package/src/fetchers/top-languages.ts +26 -40
- package/src/fetchers/types.ts +1 -1
- package/src/fetchers/wakatime.ts +5 -9
- package/src/graphql/contributionsDocument.ts +7 -5
- package/src/graphql/generated/gist.ts +1 -1
- package/src/graphql/generated/repo.ts +1 -1
- package/src/graphql/generated/stats.ts +2 -2
- package/src/graphql/generated/top-languages.ts +2 -2
- package/src/graphql/graphqlDocument.ts +0 -1
- package/src/graphql/reposContributedToDocument.ts +6 -7
- package/src/markup/css.ts +118 -0
- package/src/markup/element.ts +246 -0
- package/src/markup/html.ts +34 -0
- package/src/markup/index.ts +9 -0
- package/src/markup/serialize.ts +27 -0
- package/src/themes/index.ts +1 -1
- package/src/translations.ts +622 -629
- package/build/cards/index.d.ts +0 -11
- package/build/cards/index.d.ts.map +0 -1
- package/build/cards/index.js +0 -7
- package/build/common/html.d.ts +0 -8
- package/build/common/html.d.ts.map +0 -1
- package/build/common/html.js +0 -14
- package/build/index.d.ts +0 -15
- package/build/index.d.ts.map +0 -1
- package/build/index.js +0 -10
- package/src/_emoji-name-map.d.ts +0 -10
- package/src/_github-username-regex.d.ts +0 -4
- package/src/cards/index.ts +0 -21
- package/src/common/html.ts +0 -17
- package/src/index.ts +0 -18
|
@@ -1,30 +1,23 @@
|
|
|
1
|
-
import type { CardConfig } from '../common/config.
|
|
2
|
-
import { CardError, USER_NOT_FOUND } from '../common/error.
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { retryer } from '../common/retryer.
|
|
8
|
-
import { TopLanguagesDocument } from '../graphql/generated/top-languages.
|
|
1
|
+
import type { CardConfig } from '../common/config.ts';
|
|
2
|
+
import { CardError, USER_NOT_FOUND } from '../common/error.ts';
|
|
3
|
+
import { createGraphQLFetcher } from '../common/http.ts';
|
|
4
|
+
import { logger } from '../common/log.ts';
|
|
5
|
+
import { parseOwnerAffiliations } from '../common/ops.ts';
|
|
6
|
+
import { wrapTextMultiline } from '../common/render.ts';
|
|
7
|
+
import { retryer } from '../common/retryer.ts';
|
|
8
|
+
import { TopLanguagesDocument } from '../graphql/generated/top-languages.ts';
|
|
9
9
|
import type {
|
|
10
10
|
TopLanguageFragment,
|
|
11
11
|
TopLanguagesRepositoryFragment,
|
|
12
|
-
} from '../graphql/generated/top-languages.
|
|
12
|
+
} from '../graphql/generated/top-languages.ts';
|
|
13
13
|
|
|
14
|
-
import type { Lang, TopLangData } from './types.
|
|
14
|
+
import type { Lang, TopLangData } from './types.ts';
|
|
15
15
|
|
|
16
16
|
const fetcher = createGraphQLFetcher(TopLanguagesDocument, 'token');
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Fetch top languages for a given username.
|
|
20
20
|
*
|
|
21
|
-
* @param props Fetcher props.
|
|
22
|
-
* @param props.username GitHub username.
|
|
23
|
-
* @param props.exclude_repo List of repositories to exclude. Default: [].
|
|
24
|
-
* @param props.size_weight Weightage to be given to size.
|
|
25
|
-
* @param props.count_weight Weightage to be given to count.
|
|
26
|
-
* @param props.ownerAffiliations The owner affiliations to filter by. Default: OWNER.
|
|
27
|
-
* @param config Deployment config supplying the PAT pool.
|
|
28
21
|
* @returns Top languages data.
|
|
29
22
|
*/
|
|
30
23
|
const fetchTopLanguages = async (
|
|
@@ -59,7 +52,7 @@ const fetchTopLanguages = async (
|
|
|
59
52
|
|
|
60
53
|
if (res.data.errors) {
|
|
61
54
|
logger.error(res.data.errors);
|
|
62
|
-
const firstError = res.data.errors
|
|
55
|
+
const [firstError] = res.data.errors;
|
|
63
56
|
if (firstError?.type === 'NOT_FOUND') {
|
|
64
57
|
throw new CardError(firstError.message || 'Could not fetch user.', {
|
|
65
58
|
code: 'not_found',
|
|
@@ -82,53 +75,46 @@ const fetchTopLanguages = async (
|
|
|
82
75
|
const allExcludedRepos = [...exclude_repo, ...config.excludeRepositories];
|
|
83
76
|
|
|
84
77
|
// populate repoToHide map for quick lookup while filtering out
|
|
85
|
-
|
|
78
|
+
for (const repoName of allExcludedRepos) {
|
|
86
79
|
repoToHide[repoName] = true;
|
|
87
|
-
}
|
|
80
|
+
}
|
|
88
81
|
|
|
89
82
|
// filter out repositories to be hidden
|
|
90
83
|
const repoNodes = (res.data.data.user?.repositories.nodes ?? []).filter(
|
|
91
84
|
(node): node is TopLanguagesRepositoryFragment => !!node && !repoToHide[node.name],
|
|
92
85
|
);
|
|
93
86
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const languageEdges = repoNodes.reduce<Array<TopLanguageFragment>>((acc, repo) => {
|
|
87
|
+
let languageEdges: Array<TopLanguageFragment> = [];
|
|
88
|
+
for (const repo of repoNodes) {
|
|
97
89
|
const edges = (repo.languages?.edges ?? []).filter(
|
|
98
90
|
(edge): edge is TopLanguageFragment => !!edge,
|
|
99
91
|
);
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
if (edges.length > 0) {
|
|
93
|
+
languageEdges = [...edges, ...languageEdges];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
102
96
|
|
|
103
|
-
// accumulate size and repo count per language
|
|
97
|
+
// accumulate size and repo count per language, each read back off the language's own entry
|
|
104
98
|
const languageMap: Record<string, Lang> = {};
|
|
105
|
-
let repoCount = 0;
|
|
106
99
|
for (const edge of languageEdges) {
|
|
107
100
|
const existing = languageMap[edge.node.name];
|
|
108
|
-
|
|
109
|
-
// same language seen again: add to its size and bump count; else reset to 1
|
|
110
|
-
let langSize = edge.size;
|
|
111
|
-
if (existing && edge.node.name === existing.name) {
|
|
112
|
-
langSize = edge.size + existing.size;
|
|
113
|
-
repoCount += 1;
|
|
114
|
-
} else {
|
|
115
|
-
repoCount = 1;
|
|
116
|
-
}
|
|
117
101
|
languageMap[edge.node.name] = {
|
|
118
102
|
name: edge.node.name,
|
|
119
103
|
color: edge.node.color,
|
|
120
|
-
size:
|
|
121
|
-
count:
|
|
104
|
+
size: edge.size + (existing?.size ?? 0),
|
|
105
|
+
count: (existing?.count ?? 0) + 1,
|
|
122
106
|
};
|
|
123
107
|
}
|
|
124
108
|
|
|
125
109
|
// comparison index calculation
|
|
126
110
|
for (const lang of Object.values(languageMap)) {
|
|
127
|
-
lang.size =
|
|
111
|
+
lang.size = lang.size ** size_weight * lang.count ** count_weight;
|
|
128
112
|
}
|
|
129
113
|
|
|
130
114
|
// return languages sorted by (weighted) size, descending
|
|
131
|
-
return Object.fromEntries(
|
|
115
|
+
return Object.fromEntries(
|
|
116
|
+
Object.entries(languageMap).toSorted(([, a], [, b]) => b.size - a.size),
|
|
117
|
+
);
|
|
132
118
|
};
|
|
133
119
|
|
|
134
120
|
export { fetchTopLanguages };
|
package/src/fetchers/types.ts
CHANGED
package/src/fetchers/wakatime.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import type { CardConfig } from '../common/config.
|
|
2
|
-
import { CardError, WAKATIME_USER_NOT_FOUND } from '../common/error.
|
|
3
|
-
import { httpRequest } from '../common/http.
|
|
1
|
+
import type { CardConfig } from '../common/config.ts';
|
|
2
|
+
import { CardError, WAKATIME_USER_NOT_FOUND } from '../common/error.ts';
|
|
3
|
+
import { httpRequest } from '../common/http.ts';
|
|
4
4
|
|
|
5
|
-
import type { WakaTimeData } from './types.
|
|
5
|
+
import type { WakaTimeData } from './types.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* WakaTime data fetcher.
|
|
9
9
|
*
|
|
10
10
|
* WakaTime needs no GitHub token; the config is here for the transport it carries.
|
|
11
11
|
*
|
|
12
|
-
* @param props Fetcher props.
|
|
13
|
-
* @param props.username WakaTime username.
|
|
14
|
-
* @param props.api_domain Optional WakaTime API domain (defaults to `wakatime.com`).
|
|
15
|
-
* @param config Deployment config supplying the transport.
|
|
16
12
|
* @returns WakaTime data response.
|
|
17
13
|
*/
|
|
18
14
|
const fetchWakatimeStats = async (
|
|
@@ -29,7 +25,7 @@ const fetchWakatimeStats = async (
|
|
|
29
25
|
throw CardError.missingParam(['username']);
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
const domain = api_domain ? api_domain.
|
|
28
|
+
const domain = api_domain ? api_domain.replaceAll(/\/$/gi, '') : 'wakatime.com';
|
|
33
29
|
const res = await httpRequest<{ data: WakaTimeData }>(
|
|
34
30
|
config.fetch,
|
|
35
31
|
`https://${domain}/api/v1/users/${username}/stats?is_including_today=true`,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { getGitHubYearRange, toGitHubDateTime } from '../common/date.
|
|
1
|
+
import { getGitHubYearRange, toGitHubDateTime } from '../common/date.ts';
|
|
2
2
|
|
|
3
|
-
import type { YearContributionsFragment } from './generated/stats.
|
|
4
|
-
import {
|
|
3
|
+
import type { YearContributionsFragment } from './generated/stats.ts';
|
|
4
|
+
import type { GraphQLDocument } from './graphqlDocument.ts';
|
|
5
|
+
import { graphqlDocument } from './graphqlDocument.ts';
|
|
5
6
|
|
|
6
7
|
interface ContributionsQueryVariables {
|
|
7
8
|
login: string;
|
|
@@ -15,10 +16,11 @@ interface ContributionsQuery {
|
|
|
15
16
|
* Build the all-time contributions query, one aliased `contributionsCollection` field per year.
|
|
16
17
|
* The shape is only known at runtime.
|
|
17
18
|
*
|
|
18
|
-
* @param years Contribution years, one `year_<year>` alias each.
|
|
19
19
|
* @returns Document for `createGraphQLFetcher`.
|
|
20
20
|
*/
|
|
21
|
-
const buildContributionsDocument = (
|
|
21
|
+
const buildContributionsDocument = (
|
|
22
|
+
years: Array<number>,
|
|
23
|
+
): GraphQLDocument<ContributionsQuery, ContributionsQueryVariables> => {
|
|
22
24
|
const yearFields = years
|
|
23
25
|
.map((year) => {
|
|
24
26
|
const { from, to } = getGitHubYearRange(year);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/** Internal type. DO NOT USE DIRECTLY. */
|
|
4
4
|
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
|
5
|
-
import { graphqlDocument } from '../graphqlDocument.
|
|
5
|
+
import { graphqlDocument } from '../graphqlDocument.ts';
|
|
6
6
|
export type GistFileInfoFragment = {
|
|
7
7
|
name: string | null;
|
|
8
8
|
size: number | null;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/** Internal type. DO NOT USE DIRECTLY. */
|
|
4
4
|
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
|
5
|
-
import { graphqlDocument } from '../graphqlDocument.
|
|
5
|
+
import { graphqlDocument } from '../graphqlDocument.ts';
|
|
6
6
|
export type RepoInfoFragment = {
|
|
7
7
|
name: string;
|
|
8
8
|
nameWithOwner: string;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/** Internal type. DO NOT USE DIRECTLY. */
|
|
4
4
|
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
|
5
|
-
import { graphqlDocument } from '../graphqlDocument.
|
|
5
|
+
import { graphqlDocument } from '../graphqlDocument.ts';
|
|
6
6
|
|
|
7
|
-
import type * as Types from './common.
|
|
7
|
+
import type * as Types from './common.ts';
|
|
8
8
|
export type RepoNodeFragment = { name: string; stargazerCount: number };
|
|
9
9
|
|
|
10
10
|
export type RepoStarsFragment = {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/** Internal type. DO NOT USE DIRECTLY. */
|
|
4
4
|
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
|
5
|
-
import { graphqlDocument } from '../graphqlDocument.
|
|
5
|
+
import { graphqlDocument } from '../graphqlDocument.ts';
|
|
6
6
|
|
|
7
|
-
import type * as Types from './common.
|
|
7
|
+
import type * as Types from './common.ts';
|
|
8
8
|
export type TopLanguageFragment = { size: number; node: { color: string | null; name: string } };
|
|
9
9
|
|
|
10
10
|
export type TopLanguagesRepositoryFragment = {
|
|
@@ -14,7 +14,6 @@ interface GraphQLDocument<TResult, TVariables> {
|
|
|
14
14
|
/**
|
|
15
15
|
* Build a typed document.
|
|
16
16
|
*
|
|
17
|
-
* @param text The query text, including every fragment it spreads.
|
|
18
17
|
* @returns The document to pass to the function created by `createGraphQLFetcher`.
|
|
19
18
|
*/
|
|
20
19
|
const graphqlDocument = <TResult, TVariables>(
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { GitHubDateRange } from '../common/date.
|
|
2
|
-
import { toGitHubDateTime } from '../common/date.
|
|
1
|
+
import type { GitHubDateRange } from '../common/date.ts';
|
|
2
|
+
import { toGitHubDateTime } from '../common/date.ts';
|
|
3
3
|
|
|
4
|
-
import type { RangeContributionsByRepoFragment } from './generated/stats.
|
|
5
|
-
import {
|
|
4
|
+
import type { RangeContributionsByRepoFragment } from './generated/stats.ts';
|
|
5
|
+
import type { GraphQLDocument } from './graphqlDocument.ts';
|
|
6
|
+
import { graphqlDocument } from './graphqlDocument.ts';
|
|
6
7
|
|
|
7
8
|
/** max value GitHub allows for `first/maxRepositories` */
|
|
8
9
|
const MAX_REPOSITORIES_LIMIT = 100;
|
|
@@ -24,14 +25,12 @@ interface ReposContributedToQuery {
|
|
|
24
25
|
* Mirrors the `contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]`
|
|
25
26
|
* filter used by `repositoriesContributedTo` in `stats.graphql`.
|
|
26
27
|
*
|
|
27
|
-
* @param ranges Ranges to fetch, one `range_<index>` alias each.
|
|
28
|
-
* @param includeOwnRepos Whether to select `repositoryContributions`.
|
|
29
28
|
* @returns Document for `createGraphQLFetcher`.
|
|
30
29
|
*/
|
|
31
30
|
const buildReposContributedToDocument = (
|
|
32
31
|
ranges: Array<GitHubDateRange>,
|
|
33
32
|
includeOwnRepos: boolean,
|
|
34
|
-
) => {
|
|
33
|
+
): GraphQLDocument<ReposContributedToQuery, ReposContributedToQueryVariables> => {
|
|
35
34
|
const rangeFields = ranges
|
|
36
35
|
.map(
|
|
37
36
|
({ from, to }, index) =>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Sink } from './serialize.ts';
|
|
2
|
+
import { INDENT, indentAt } from './serialize.ts';
|
|
3
|
+
|
|
4
|
+
/** Every CSS property a card declares, closed so a typo is a compile error, not a dropped rule. */
|
|
5
|
+
type CssProperty =
|
|
6
|
+
| '-webkit-box-orient'
|
|
7
|
+
| '-webkit-line-clamp'
|
|
8
|
+
| 'animation'
|
|
9
|
+
| 'animation-delay'
|
|
10
|
+
| 'animation-duration'
|
|
11
|
+
| 'color'
|
|
12
|
+
| 'display'
|
|
13
|
+
| 'fill'
|
|
14
|
+
| 'font'
|
|
15
|
+
| 'font-size'
|
|
16
|
+
| 'font-variant-numeric'
|
|
17
|
+
| 'font-weight'
|
|
18
|
+
| 'line-clamp'
|
|
19
|
+
| 'line-height'
|
|
20
|
+
| 'margin'
|
|
21
|
+
| 'opacity'
|
|
22
|
+
| 'overflow'
|
|
23
|
+
| 'overflow-wrap'
|
|
24
|
+
| 'padding-bottom'
|
|
25
|
+
| 'stroke'
|
|
26
|
+
| 'stroke-dasharray'
|
|
27
|
+
| 'stroke-dashoffset'
|
|
28
|
+
| 'stroke-linecap'
|
|
29
|
+
| 'stroke-width'
|
|
30
|
+
| 'text-overflow'
|
|
31
|
+
| 'transform'
|
|
32
|
+
| 'transform-origin'
|
|
33
|
+
| 'width'
|
|
34
|
+
| 'word-break';
|
|
35
|
+
|
|
36
|
+
/** A declaration whose value is `undefined` is left out, so a caller can inline a condition. */
|
|
37
|
+
type Declarations = Partial<Record<CssProperty, string | number | undefined>>;
|
|
38
|
+
|
|
39
|
+
type CssNode =
|
|
40
|
+
| { readonly kind: 'rule'; readonly selector: string; readonly declarations: Declarations }
|
|
41
|
+
| { readonly kind: 'at'; readonly prelude: string; readonly children: Array<CssChild> }
|
|
42
|
+
| { readonly kind: 'comment'; readonly text: string };
|
|
43
|
+
|
|
44
|
+
/** Anything accepted where CSS is expected; falsy entries drop out, arrays flatten. */
|
|
45
|
+
type CssChild = CssNode | false | null | undefined | Array<CssChild>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Selectors stay free-form: they compose (`.badge rect`), and a keyframe stop is one too (`from`).
|
|
49
|
+
*/
|
|
50
|
+
const rule = (selector: string, declarations: Declarations): CssNode => ({
|
|
51
|
+
kind: 'rule',
|
|
52
|
+
selector,
|
|
53
|
+
declarations,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const atRule = (prelude: string, ...children: Array<CssChild>): CssNode => ({
|
|
57
|
+
kind: 'at',
|
|
58
|
+
prelude,
|
|
59
|
+
children,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const cssComment = (text: string): CssNode => ({ kind: 'comment', text });
|
|
63
|
+
|
|
64
|
+
/** Drops the rules that carry nothing and flattens the arrays. */
|
|
65
|
+
const flatten = (children: Array<CssChild>, into: Array<CssNode> = []): Array<CssNode> => {
|
|
66
|
+
for (const child of children) {
|
|
67
|
+
if (!child) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(child)) {
|
|
71
|
+
flatten(child, into);
|
|
72
|
+
} else {
|
|
73
|
+
into.push(child);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return into;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Serializes a stylesheet, dropping every rule that would have come out empty.
|
|
81
|
+
*
|
|
82
|
+
* A block is written optimistically and rewound if its body turns out empty,
|
|
83
|
+
* so the tree is walked once however deeply the at-rules nest.
|
|
84
|
+
*/
|
|
85
|
+
const writeCss = (children: Array<CssChild>, depth: number, sink: Sink): void => {
|
|
86
|
+
const indent = indentAt(depth);
|
|
87
|
+
|
|
88
|
+
for (const node of flatten(children)) {
|
|
89
|
+
if (node.kind === 'comment') {
|
|
90
|
+
sink.out += `${indent}/* ${node.text} */\n`;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mark = sink.out.length;
|
|
95
|
+
|
|
96
|
+
if (node.kind === 'at') {
|
|
97
|
+
sink.out += `${indent}${node.prelude} {\n`;
|
|
98
|
+
const bodyStart = sink.out.length;
|
|
99
|
+
writeCss(node.children, depth + 1, sink);
|
|
100
|
+
sink.out =
|
|
101
|
+
sink.out.length === bodyStart ? sink.out.slice(0, mark) : `${sink.out}${indent}}\n`;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
sink.out += `${indent}${node.selector} {\n`;
|
|
106
|
+
const bodyStart = sink.out.length;
|
|
107
|
+
for (const property of Object.keys(node.declarations) as Array<CssProperty>) {
|
|
108
|
+
const value = node.declarations[property];
|
|
109
|
+
if (value !== undefined) {
|
|
110
|
+
sink.out += `${indent}${INDENT}${property}: ${String(value)};\n`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
sink.out = sink.out.length === bodyStart ? sink.out.slice(0, mark) : `${sink.out}${indent}}\n`;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export { atRule, cssComment, rule, writeCss };
|
|
118
|
+
export type { CssChild, Declarations };
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { CssChild } from './css.ts';
|
|
2
|
+
import { writeCss } from './css.ts';
|
|
3
|
+
import { encodeHTML } from './html.ts';
|
|
4
|
+
import type { Sink } from './serialize.ts';
|
|
5
|
+
import { indentAt } from './serialize.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every element a card draws, closed so `el('circel', …)` is a compile error.
|
|
9
|
+
* `style` is absent on purpose: it comes from `style()`, which takes rules rather than children.
|
|
10
|
+
*/
|
|
11
|
+
type Tag =
|
|
12
|
+
| 'a'
|
|
13
|
+
| 'circle'
|
|
14
|
+
| 'defs'
|
|
15
|
+
| 'desc'
|
|
16
|
+
| 'div'
|
|
17
|
+
| 'foreignObject'
|
|
18
|
+
| 'g'
|
|
19
|
+
| 'linearGradient'
|
|
20
|
+
| 'mask'
|
|
21
|
+
| 'path'
|
|
22
|
+
| 'rect'
|
|
23
|
+
| 'stop'
|
|
24
|
+
| 'svg'
|
|
25
|
+
| 'text'
|
|
26
|
+
| 'title'
|
|
27
|
+
| 'tspan';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every attribute a card writes, closed like `Tag`:
|
|
31
|
+
* a misspelled `stroke-width` would otherwise be valid TypeScript and invisible in the output.
|
|
32
|
+
*/
|
|
33
|
+
type Attribute =
|
|
34
|
+
| 'alignment-baseline'
|
|
35
|
+
| 'aria-hidden'
|
|
36
|
+
| 'aria-labelledby'
|
|
37
|
+
| 'class'
|
|
38
|
+
| 'cx'
|
|
39
|
+
| 'cy'
|
|
40
|
+
| 'd'
|
|
41
|
+
| 'data-testid'
|
|
42
|
+
| 'data-view-component'
|
|
43
|
+
| 'dominant-baseline'
|
|
44
|
+
| 'dy'
|
|
45
|
+
| 'fill'
|
|
46
|
+
| 'fill-rule'
|
|
47
|
+
| 'gradientTransform'
|
|
48
|
+
| 'gradientUnits'
|
|
49
|
+
| 'height'
|
|
50
|
+
| 'href'
|
|
51
|
+
| 'id'
|
|
52
|
+
| 'mask'
|
|
53
|
+
| 'offset'
|
|
54
|
+
| 'r'
|
|
55
|
+
| 'role'
|
|
56
|
+
| 'rx'
|
|
57
|
+
| 'ry'
|
|
58
|
+
| 'size'
|
|
59
|
+
| 'stop-color'
|
|
60
|
+
| 'stroke'
|
|
61
|
+
| 'stroke-dasharray'
|
|
62
|
+
| 'stroke-dashoffset'
|
|
63
|
+
| 'stroke-linecap'
|
|
64
|
+
| 'stroke-opacity'
|
|
65
|
+
| 'stroke-width'
|
|
66
|
+
| 'style'
|
|
67
|
+
| 'text-anchor'
|
|
68
|
+
| 'transform'
|
|
69
|
+
| 'version'
|
|
70
|
+
| 'viewBox'
|
|
71
|
+
| 'width'
|
|
72
|
+
| 'x'
|
|
73
|
+
| 'xmlns'
|
|
74
|
+
| 'y';
|
|
75
|
+
|
|
76
|
+
/** An attribute whose value is `undefined` is not written, so a caller can inline a condition. */
|
|
77
|
+
type Attributes = Partial<Record<Attribute, string | number | undefined>>;
|
|
78
|
+
|
|
79
|
+
/** Elements whose character data is part of the rendering, so they stay on one line. */
|
|
80
|
+
const INLINE_ELEMENTS: ReadonlySet<Tag> = new Set(['text', 'tspan', 'title', 'desc', 'div']);
|
|
81
|
+
|
|
82
|
+
interface MarkupElement {
|
|
83
|
+
readonly tag: Tag;
|
|
84
|
+
readonly attributes: Attributes;
|
|
85
|
+
readonly children: Array<Child>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface StyleElement {
|
|
89
|
+
readonly tag: 'style';
|
|
90
|
+
readonly rules: Array<CssChild>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Anything accepted as content; falsy entries drop out, arrays flatten, strings are escaped. */
|
|
94
|
+
type Child =
|
|
95
|
+
| MarkupElement
|
|
96
|
+
| StyleElement
|
|
97
|
+
| string
|
|
98
|
+
| number
|
|
99
|
+
| false
|
|
100
|
+
| null
|
|
101
|
+
| undefined
|
|
102
|
+
| Array<Child>;
|
|
103
|
+
|
|
104
|
+
/** What is left of a child list once the empty entries and the nesting are gone. */
|
|
105
|
+
type FlatChild = MarkupElement | StyleElement | string | number;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Builds one element. Attribute values and text children are encoded on the way out.
|
|
109
|
+
*/
|
|
110
|
+
const el = (tag: Tag, attributes: Attributes = {}, ...children: Array<Child>): MarkupElement => ({
|
|
111
|
+
tag,
|
|
112
|
+
attributes,
|
|
113
|
+
children,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds the `<style>` element from a stylesheet rather than from text.
|
|
118
|
+
*/
|
|
119
|
+
const style = (...rules: Array<CssChild>): StyleElement => ({ tag: 'style', rules });
|
|
120
|
+
|
|
121
|
+
const isStyle = (child: MarkupElement | StyleElement): child is StyleElement => 'rules' in child;
|
|
122
|
+
|
|
123
|
+
/** Drops the children that carry nothing and flattens the arrays. */
|
|
124
|
+
const flatten = (children: Array<Child>, into: Array<FlatChild> = []): Array<FlatChild> => {
|
|
125
|
+
for (const child of children) {
|
|
126
|
+
if (child === false || child === null || child === undefined || child === '') {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(child)) {
|
|
130
|
+
flatten(child, into);
|
|
131
|
+
} else {
|
|
132
|
+
into.push(child);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return into;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const openTag = (node: MarkupElement, selfClosing: boolean): string => {
|
|
139
|
+
let tag = `<${node.tag}`;
|
|
140
|
+
const { attributes } = node;
|
|
141
|
+
|
|
142
|
+
// `Object.keys` widens to `string`, so the cast restores what the type already says.
|
|
143
|
+
for (const name of Object.keys(attributes) as Array<Attribute>) {
|
|
144
|
+
const value = attributes[name];
|
|
145
|
+
if (value !== undefined) {
|
|
146
|
+
// A number can hold nothing that needs escaping, so it skips the scan.
|
|
147
|
+
tag += typeof value === 'number' ? ` ${name}="${value}"` : ` ${name}="${encodeHTML(value)}"`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return `${tag}${selfClosing ? ' />' : '>'}`;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Serializes an element and its descendants onto one line, whitespace and all. */
|
|
155
|
+
const writeInline = (node: FlatChild): string => {
|
|
156
|
+
if (typeof node === 'string') {
|
|
157
|
+
return encodeHTML(node);
|
|
158
|
+
}
|
|
159
|
+
if (typeof node === 'number') {
|
|
160
|
+
return String(node);
|
|
161
|
+
}
|
|
162
|
+
if (isStyle(node)) {
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const children = flatten(node.children);
|
|
167
|
+
if (children.length === 0) {
|
|
168
|
+
return openTag(node, true);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let inner = '';
|
|
172
|
+
for (const child of children) {
|
|
173
|
+
inner += writeInline(child);
|
|
174
|
+
}
|
|
175
|
+
return `${openTag(node, false)}${inner}</${node.tag}>`;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A container is written optimistically and rewound if its content turns out empty,
|
|
180
|
+
* so an element that draws nothing costs one walk rather than two.
|
|
181
|
+
*/
|
|
182
|
+
const write = (node: FlatChild, depth: number, sink: Sink): void => {
|
|
183
|
+
const indent = indentAt(depth);
|
|
184
|
+
|
|
185
|
+
if (typeof node === 'string') {
|
|
186
|
+
sink.out += `${indent}${encodeHTML(node)}\n`;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (typeof node === 'number') {
|
|
190
|
+
sink.out += `${indent}${node}\n`;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const mark = sink.out.length;
|
|
195
|
+
|
|
196
|
+
if (isStyle(node)) {
|
|
197
|
+
sink.out += `${indent}<style>\n`;
|
|
198
|
+
const bodyStart = sink.out.length;
|
|
199
|
+
writeCss(node.rules, depth + 1, sink);
|
|
200
|
+
sink.out =
|
|
201
|
+
sink.out.length === bodyStart ? sink.out.slice(0, mark) : `${sink.out}${indent}</style>\n`;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const children = flatten(node.children);
|
|
206
|
+
if (children.length === 0) {
|
|
207
|
+
sink.out += `${indent}${openTag(node, true)}\n`;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (INLINE_ELEMENTS.has(node.tag)) {
|
|
212
|
+
let inner = '';
|
|
213
|
+
for (const child of children) {
|
|
214
|
+
inner += writeInline(child);
|
|
215
|
+
}
|
|
216
|
+
sink.out +=
|
|
217
|
+
inner === ''
|
|
218
|
+
? `${indent}${openTag(node, true)}\n`
|
|
219
|
+
: `${indent}${openTag(node, false)}${inner}</${node.tag}>\n`;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
sink.out += `${indent}${openTag(node, false)}\n`;
|
|
224
|
+
const bodyStart = sink.out.length;
|
|
225
|
+
for (const child of children) {
|
|
226
|
+
write(child, depth + 1, sink);
|
|
227
|
+
}
|
|
228
|
+
sink.out =
|
|
229
|
+
sink.out.length === bodyStart
|
|
230
|
+
? `${sink.out.slice(0, mark)}${indent}${openTag(node, true)}\n`
|
|
231
|
+
: `${sink.out}${indent}</${node.tag}>\n`;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Serializes a finished card: one element per line, two spaces per nesting level.
|
|
236
|
+
*
|
|
237
|
+
* @returns The rendered SVG.
|
|
238
|
+
*/
|
|
239
|
+
const renderMarkup = (root: MarkupElement): string => {
|
|
240
|
+
const sink: Sink = { out: '' };
|
|
241
|
+
write(root, 0, sink);
|
|
242
|
+
return sink.out;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export { el, renderMarkup, style };
|
|
246
|
+
export type { Child, MarkupElement };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What has to be encoded: the markup characters, everything above plain ASCII, and a backspace.
|
|
3
|
+
* One source for both regexes, so the check and the replacement cannot disagree.
|
|
4
|
+
*/
|
|
5
|
+
const ENCODED = String.raw`[<>&"'\u00A0-\u9999\u0008]`;
|
|
6
|
+
|
|
7
|
+
/** Non-global, so `.test` stays stateless: a global regex would carry `lastIndex`. */
|
|
8
|
+
const NEEDS_ENCODING = new RegExp(ENCODED);
|
|
9
|
+
const TO_ENCODE = new RegExp(ENCODED, 'g');
|
|
10
|
+
|
|
11
|
+
const BACKSPACE = '\u0008';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Encodes a string for use as markup, one numeric character reference each.
|
|
15
|
+
*
|
|
16
|
+
* The serializer is now the only place text becomes markup, so nothing arrives pre-escaped:
|
|
17
|
+
* a literal `&` is encoded like any other `&` rather than passed through as a reference.
|
|
18
|
+
*
|
|
19
|
+
* @see https://stackoverflow.com/a/48073476/10629172
|
|
20
|
+
*
|
|
21
|
+
* @returns The text, safe to place in an attribute or as character data.
|
|
22
|
+
*/
|
|
23
|
+
const encodeHTML = (str: string): string => {
|
|
24
|
+
// Most strings a card writes are plain ASCII, so they leave without a rewrite.
|
|
25
|
+
if (!NEEDS_ENCODING.test(str)) {
|
|
26
|
+
return str;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return str.replace(TO_ENCODE, (char) =>
|
|
30
|
+
char === BACKSPACE ? '' : `&#${char.codePointAt(0) ?? 0};`,
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { encodeHTML };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markup layer: a card is built as a tree of elements and rules, then serialized once.
|
|
3
|
+
* It depends on nothing else in the package, so it sits below `cards/`, not inside `common/`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { atRule, cssComment, rule } from './css.ts';
|
|
7
|
+
export type { CssChild, Declarations } from './css.ts';
|
|
8
|
+
export { el, renderMarkup, style } from './element.ts';
|
|
9
|
+
export type { Child, MarkupElement } from './element.ts';
|