@urmzd/github-insights 2.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.
Files changed (102) hide show
  1. package/.gitattributes +28 -0
  2. package/.github/dependabot.yml +6 -0
  3. package/.github/pull_request_template.md +14 -0
  4. package/.github/workflows/ci.yml +93 -0
  5. package/.github/workflows/release.yml +59 -0
  6. package/.nvmrc +1 -0
  7. package/.pre-commit-config.yaml +5 -0
  8. package/AGENTS.md +69 -0
  9. package/CHANGELOG.md +260 -0
  10. package/CONTRIBUTING.md +87 -0
  11. package/LICENSE +190 -0
  12. package/README.md +188 -0
  13. package/action.yml +45 -0
  14. package/biome.json +40 -0
  15. package/examples/classic/README.md +9 -0
  16. package/examples/classic/index.svg +14 -0
  17. package/examples/classic/metrics-calendar.svg +14 -0
  18. package/examples/classic/metrics-complexity.svg +14 -0
  19. package/examples/classic/metrics-contributions.svg +14 -0
  20. package/examples/classic/metrics-expertise.svg +14 -0
  21. package/examples/classic/metrics-languages.svg +14 -0
  22. package/examples/classic/metrics-pulse.svg +14 -0
  23. package/examples/ecosystem/README.md +59 -0
  24. package/examples/ecosystem/index.svg +14 -0
  25. package/examples/ecosystem/metrics-calendar.svg +14 -0
  26. package/examples/ecosystem/metrics-complexity.svg +14 -0
  27. package/examples/ecosystem/metrics-contributions.svg +14 -0
  28. package/examples/ecosystem/metrics-expertise.svg +14 -0
  29. package/examples/ecosystem/metrics-languages.svg +14 -0
  30. package/examples/ecosystem/metrics-pulse.svg +14 -0
  31. package/examples/minimal/README.md +9 -0
  32. package/examples/minimal/index.svg +14 -0
  33. package/examples/minimal/metrics-calendar.svg +14 -0
  34. package/examples/minimal/metrics-complexity.svg +14 -0
  35. package/examples/minimal/metrics-contributions.svg +14 -0
  36. package/examples/minimal/metrics-expertise.svg +14 -0
  37. package/examples/minimal/metrics-languages.svg +14 -0
  38. package/examples/minimal/metrics-pulse.svg +14 -0
  39. package/examples/modern/README.md +111 -0
  40. package/examples/modern/index.svg +14 -0
  41. package/examples/modern/metrics-calendar.svg +14 -0
  42. package/examples/modern/metrics-complexity.svg +14 -0
  43. package/examples/modern/metrics-contributions.svg +14 -0
  44. package/examples/modern/metrics-expertise.svg +14 -0
  45. package/examples/modern/metrics-languages.svg +14 -0
  46. package/examples/modern/metrics-pulse.svg +14 -0
  47. package/llms.txt +24 -0
  48. package/metrics/index.svg +14 -0
  49. package/metrics/metrics-calendar.svg +14 -0
  50. package/metrics/metrics-complexity.svg +14 -0
  51. package/metrics/metrics-contributions.svg +14 -0
  52. package/metrics/metrics-domains.svg +14 -0
  53. package/metrics/metrics-expertise.svg +14 -0
  54. package/metrics/metrics-languages.svg +14 -0
  55. package/metrics/metrics-pulse.svg +14 -0
  56. package/metrics/metrics-tech-stack.svg +14 -0
  57. package/package.json +35 -0
  58. package/skills/github-insights/SKILL.md +237 -0
  59. package/sr.yaml +16 -0
  60. package/src/__fixtures__/repos.ts +84 -0
  61. package/src/api.ts +729 -0
  62. package/src/components/bar-chart.test.tsx +38 -0
  63. package/src/components/bar-chart.tsx +54 -0
  64. package/src/components/contribution-calendar.test.tsx +44 -0
  65. package/src/components/contribution-calendar.tsx +94 -0
  66. package/src/components/contribution-cards.test.tsx +36 -0
  67. package/src/components/contribution-cards.tsx +58 -0
  68. package/src/components/donut-chart.test.tsx +36 -0
  69. package/src/components/donut-chart.tsx +102 -0
  70. package/src/components/full-svg.test.tsx +54 -0
  71. package/src/components/full-svg.tsx +59 -0
  72. package/src/components/project-cards.test.tsx +46 -0
  73. package/src/components/project-cards.tsx +66 -0
  74. package/src/components/section.test.tsx +69 -0
  75. package/src/components/section.tsx +79 -0
  76. package/src/components/stat-cards.test.tsx +32 -0
  77. package/src/components/stat-cards.tsx +57 -0
  78. package/src/components/style-defs.test.tsx +26 -0
  79. package/src/components/style-defs.tsx +27 -0
  80. package/src/components/tech-highlights.test.tsx +63 -0
  81. package/src/components/tech-highlights.tsx +109 -0
  82. package/src/config.test.ts +127 -0
  83. package/src/config.ts +103 -0
  84. package/src/index.ts +363 -0
  85. package/src/jsx-factory.test.tsx +86 -0
  86. package/src/jsx-factory.ts +46 -0
  87. package/src/jsx.d.ts +6 -0
  88. package/src/metrics.test.ts +669 -0
  89. package/src/metrics.ts +365 -0
  90. package/src/parsers.test.ts +247 -0
  91. package/src/parsers.ts +146 -0
  92. package/src/readme.test.ts +189 -0
  93. package/src/readme.ts +70 -0
  94. package/src/svg-utils.test.ts +66 -0
  95. package/src/svg-utils.ts +33 -0
  96. package/src/templates.test.ts +412 -0
  97. package/src/templates.ts +296 -0
  98. package/src/theme.ts +33 -0
  99. package/src/types.ts +235 -0
  100. package/teasr.toml +14 -0
  101. package/tsconfig.json +21 -0
  102. package/vitest.config.ts +12 -0
package/src/index.ts ADDED
@@ -0,0 +1,363 @@
1
+ import { copyFileSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative } from "node:path";
3
+ import * as core from "@actions/core";
4
+ import * as exec from "@actions/exec";
5
+ import {
6
+ fetchAIPreamble,
7
+ fetchAllRepoData,
8
+ fetchContributionData,
9
+ fetchExpertiseAnalysis,
10
+ fetchManifestsForRepos,
11
+ fetchProjectClassifications,
12
+ fetchReadmeForRepos,
13
+ fetchUserProfile,
14
+ makeGraphql,
15
+ } from "./api.js";
16
+ import { generateFullSvg, wrapSectionSvg } from "./components/full-svg.js";
17
+ import { renderSection } from "./components/section.js";
18
+ import { loadUserConfig } from "./config.js";
19
+ import {
20
+ aggregateLanguages,
21
+ buildClassificationInputs,
22
+ buildSections,
23
+ collectAllDependencies,
24
+ collectAllTopics,
25
+ getTopProjectsByComplexity,
26
+ SECTION_KEYS,
27
+ splitProjectsByRecency,
28
+ } from "./metrics.js";
29
+ import { loadPreamble } from "./readme.js";
30
+ import {
31
+ buildSocialBadges,
32
+ extractFirstName,
33
+ getTemplate,
34
+ } from "./templates.js";
35
+ import type { TemplateName } from "./types.js";
36
+
37
+ async function run(): Promise<void> {
38
+ try {
39
+ const token =
40
+ core.getInput("github-token") || process.env.GITHUB_TOKEN || "";
41
+ const username =
42
+ core.getInput("username") || process.env.GITHUB_REPOSITORY_OWNER || "";
43
+ const outputDir = core.getInput("output-dir") || "assets/insights";
44
+ const commitPush =
45
+ (core.getInput("commit-push") || (process.env.CI ? "true" : "false")) ===
46
+ "true";
47
+ const commitMessage =
48
+ core.getInput("commit-message") || "chore: update metrics";
49
+ const commitName = core.getInput("commit-name") || "github-actions[bot]";
50
+ const commitEmail =
51
+ core.getInput("commit-email") ||
52
+ "41898282+github-actions[bot]@users.noreply.github.com";
53
+ const configPath = core.getInput("config-file") || undefined;
54
+ const readmePath =
55
+ core.getInput("readme-path") ||
56
+ (process.env.CI ? "README.md" : "_README.md");
57
+ const indexOnly = (core.getInput("index-only") || "true") === "true";
58
+ const userConfig = loadUserConfig(configPath);
59
+
60
+ // Template and sections from action inputs or config
61
+ const templateName: TemplateName =
62
+ (core.getInput("template") as TemplateName) ||
63
+ userConfig.template ||
64
+ "classic";
65
+ const sectionsInput = core.getInput("sections") || "";
66
+ const requestedSections =
67
+ sectionsInput.length > 0
68
+ ? sectionsInput
69
+ .split(",")
70
+ .map((s) => s.trim().toLowerCase())
71
+ .filter(Boolean)
72
+ : userConfig.sections || [];
73
+
74
+ if (!token) {
75
+ core.setFailed("github-token is required");
76
+ return;
77
+ }
78
+ if (!username) {
79
+ core.setFailed("username is required");
80
+ return;
81
+ }
82
+
83
+ // ── Fetch ─────────────────────────────────────────────────────────────
84
+ const graphql = makeGraphql(token);
85
+
86
+ core.info("Fetching repo data...");
87
+ const repos = await fetchAllRepoData(graphql, username);
88
+ core.info(`Found ${repos.length} public repos`);
89
+
90
+ core.info("Fetching dependency manifests...");
91
+ core.info("Fetching contribution data...");
92
+ core.info("Fetching READMEs...");
93
+ core.info("Fetching user profile...");
94
+ const [manifests, contributionData, readmeMap, userProfile] =
95
+ await Promise.all([
96
+ fetchManifestsForRepos(graphql, username, repos),
97
+ fetchContributionData(graphql, username),
98
+ fetchReadmeForRepos(graphql, username, repos),
99
+ fetchUserProfile(graphql, username),
100
+ ]);
101
+ core.info(`Fetched manifests for ${manifests.size} repos`);
102
+ core.info(
103
+ `Contributions: ${contributionData.contributions.totalCommitContributions} commits, ${contributionData.contributions.totalPullRequestContributions} PRs`,
104
+ );
105
+ core.info(`Fetched READMEs for ${readmeMap.size} repos`);
106
+ core.info(`User profile: ${userProfile.name || username}`);
107
+
108
+ // ── Transform ─────────────────────────────────────────────────────────
109
+ const languages = aggregateLanguages(repos);
110
+ const complexProjects = getTopProjectsByComplexity(repos);
111
+ const projects = complexProjects.slice(0, 5);
112
+
113
+ const allDeps = collectAllDependencies(repos, manifests);
114
+ const allTopics = collectAllTopics(repos);
115
+
116
+ core.info("Fetching project classifications from GitHub Models...");
117
+ const classificationInputs = buildClassificationInputs(
118
+ repos,
119
+ contributionData,
120
+ );
121
+ const [aiClassifications, techHighlights] = await Promise.all([
122
+ fetchProjectClassifications(token, classificationInputs),
123
+ (async () => {
124
+ core.info("Fetching expertise analysis from GitHub Models...");
125
+ return fetchExpertiseAnalysis(
126
+ token,
127
+ languages,
128
+ allDeps,
129
+ allTopics,
130
+ repos,
131
+ readmeMap,
132
+ userConfig,
133
+ );
134
+ })(),
135
+ ]);
136
+ core.info(
137
+ `Project classifications: ${aiClassifications.length} AI-classified (${repos.length - aiClassifications.length} heuristic fallback)`,
138
+ );
139
+ core.info(`Expertise analysis: ${techHighlights.length} categories`);
140
+
141
+ const {
142
+ active: activeProjects,
143
+ maintained: maintainedProjects,
144
+ inactive: inactiveProjects,
145
+ } = splitProjectsByRecency(repos, contributionData, aiClassifications);
146
+
147
+ const sectionDefs = buildSections({
148
+ languages,
149
+ techHighlights,
150
+ projects,
151
+ contributionData,
152
+ });
153
+
154
+ // Filter sections by requested keys if specified
155
+ let activeSections = sectionDefs.filter(
156
+ (s) => s.renderBody || (s.items && s.items.length > 0),
157
+ );
158
+ if (requestedSections.length > 0) {
159
+ const allowedFilenames = new Set(
160
+ requestedSections.map((key) => SECTION_KEYS[key]).filter(Boolean),
161
+ );
162
+ activeSections = activeSections.filter((s) =>
163
+ allowedFilenames.has(s.filename),
164
+ );
165
+ }
166
+
167
+ // ── Render + Write ────────────────────────────────────────────────────
168
+ mkdirSync(outputDir, { recursive: true });
169
+
170
+ for (const section of activeSections) {
171
+ const { svg, height } = renderSection(
172
+ section.title,
173
+ section.subtitle,
174
+ section.renderBody || section.items || [],
175
+ section.options || {},
176
+ );
177
+ writeFileSync(
178
+ `${outputDir}/${section.filename}`,
179
+ wrapSectionSvg(svg, height),
180
+ );
181
+ core.info(`Wrote ${outputDir}/${section.filename}`);
182
+ }
183
+
184
+ const combinedSvg = generateFullSvg(activeSections);
185
+ writeFileSync(`${outputDir}/index.svg`, combinedSvg);
186
+ core.info(`Wrote ${outputDir}/index.svg`);
187
+
188
+ // ── README ─────────────────────────────────────────────────────────────
189
+ if (readmePath && readmePath !== "none") {
190
+ const svgDir = relative(dirname(readmePath), outputDir) || ".";
191
+
192
+ let preamble = loadPreamble(userConfig.preamble);
193
+
194
+ if (!preamble) {
195
+ core.info("No PREAMBLE.md found, generating with AI...");
196
+ preamble = await fetchAIPreamble(token, {
197
+ username,
198
+ profile: userProfile,
199
+ userConfig,
200
+ languages,
201
+ techHighlights,
202
+ activeProjects,
203
+ complexProjects,
204
+ });
205
+ }
206
+
207
+ const svgs = indexOnly
208
+ ? [{ label: "GitHub Metrics", path: `${svgDir}/index.svg` }]
209
+ : activeSections.map((s) => ({
210
+ label: s.title,
211
+ path: `${svgDir}/${s.filename}`,
212
+ }));
213
+
214
+ // Build section SVG path map for templates
215
+ const sectionSvgs: Record<string, string> = {};
216
+ for (const [key, filename] of Object.entries(SECTION_KEYS)) {
217
+ if (activeSections.some((s) => s.filename === filename)) {
218
+ sectionSvgs[key] = `${svgDir}/${filename}`;
219
+ }
220
+ }
221
+
222
+ const displayName = userConfig.name || userProfile.name || username;
223
+ const socialBadges = buildSocialBadges(userProfile);
224
+
225
+ // Build categorized projects map for ecosystem template
226
+ const allProjectItems = [
227
+ ...activeProjects,
228
+ ...maintainedProjects,
229
+ ...inactiveProjects,
230
+ ];
231
+ const categorizedProjects: Record<string, typeof allProjectItems> = {};
232
+ for (const project of allProjectItems) {
233
+ const cat = project.category || "Other";
234
+ if (!categorizedProjects[cat]) categorizedProjects[cat] = [];
235
+ categorizedProjects[cat].push(project);
236
+ }
237
+
238
+ {
239
+ const template = getTemplate(templateName);
240
+ const readme = template({
241
+ username,
242
+ name: displayName,
243
+ firstName: extractFirstName(displayName),
244
+ pronunciation: userConfig.pronunciation,
245
+ title: userConfig.title,
246
+ bio: userConfig.bio,
247
+ preamble,
248
+ svgs,
249
+ sectionSvgs,
250
+ profile: userProfile,
251
+ activeProjects,
252
+ maintainedProjects,
253
+ inactiveProjects,
254
+ allProjects: complexProjects,
255
+ categorizedProjects,
256
+ languages,
257
+ techHighlights,
258
+ contributionData,
259
+ socialBadges,
260
+ svgDir,
261
+ });
262
+ writeFileSync(readmePath, readme);
263
+ }
264
+
265
+ core.info(`Wrote ${readmePath} (template: ${templateName})`);
266
+
267
+ // ── Local template previews ──────────────────────────────────────────
268
+ if (!process.env.CI) {
269
+ const allTemplateNames: TemplateName[] = [
270
+ "classic",
271
+ "modern",
272
+ "minimal",
273
+ "ecosystem",
274
+ ];
275
+ for (const tplName of allTemplateNames) {
276
+ const tplDir = `examples/${tplName}`;
277
+ mkdirSync(tplDir, { recursive: true });
278
+
279
+ // Copy SVGs into the subfolder
280
+ copyFileSync(`${outputDir}/index.svg`, `${tplDir}/index.svg`);
281
+ for (const section of activeSections) {
282
+ copyFileSync(
283
+ `${outputDir}/${section.filename}`,
284
+ `${tplDir}/${section.filename}`,
285
+ );
286
+ }
287
+
288
+ const previewSvgs = indexOnly
289
+ ? [{ label: "GitHub Metrics", path: `./index.svg` }]
290
+ : activeSections.map((s) => ({
291
+ label: s.title,
292
+ path: `./${s.filename}`,
293
+ }));
294
+
295
+ const previewSectionSvgs: Record<string, string> = {};
296
+ for (const [key, filename] of Object.entries(SECTION_KEYS)) {
297
+ if (activeSections.some((s) => s.filename === filename)) {
298
+ previewSectionSvgs[key] = `./${filename}`;
299
+ }
300
+ }
301
+
302
+ const template = getTemplate(tplName);
303
+ const output = template({
304
+ username,
305
+ name: displayName,
306
+ firstName: extractFirstName(displayName),
307
+ pronunciation: userConfig.pronunciation,
308
+ title: userConfig.title,
309
+ bio: userConfig.bio,
310
+ preamble,
311
+ svgs: previewSvgs,
312
+ sectionSvgs: previewSectionSvgs,
313
+ profile: userProfile,
314
+ activeProjects,
315
+ maintainedProjects,
316
+ inactiveProjects,
317
+ allProjects: complexProjects,
318
+ categorizedProjects,
319
+ languages,
320
+ techHighlights,
321
+ contributionData,
322
+ socialBadges,
323
+ svgDir: ".",
324
+ });
325
+
326
+ const previewPath = `${tplDir}/README.md`;
327
+ writeFileSync(previewPath, output);
328
+ core.info(`Wrote ${previewPath} (template preview: ${tplName})`);
329
+ }
330
+ }
331
+ }
332
+
333
+ // ── Commit + Push ─────────────────────────────────────────────────────
334
+ if (commitPush) {
335
+ await exec.exec("git", ["config", "user.name", commitName]);
336
+ await exec.exec("git", ["config", "user.email", commitEmail]);
337
+ const filesToAdd = [`${outputDir}/`];
338
+ if (readmePath && readmePath !== "none") {
339
+ filesToAdd.push(readmePath);
340
+ }
341
+ await exec.exec("git", ["add", ...filesToAdd]);
342
+
343
+ const diffResult = await exec.exec(
344
+ "git",
345
+ ["diff", "--staged", "--quiet"],
346
+ { ignoreReturnCode: true },
347
+ );
348
+
349
+ if (diffResult !== 0) {
350
+ await exec.exec("git", ["commit", "-m", commitMessage]);
351
+ await exec.exec("git", ["push"]);
352
+ core.info("Changes committed and pushed.");
353
+ } else {
354
+ core.info("No changes to commit.");
355
+ }
356
+ }
357
+ } catch (error: unknown) {
358
+ const msg = error instanceof Error ? error.message : String(error);
359
+ core.setFailed(msg);
360
+ }
361
+ }
362
+
363
+ run();
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Fragment, h } from "./jsx-factory.js";
3
+
4
+ describe("h – HTML tags", () => {
5
+ it("renders self-closing tag (rect)", () => {
6
+ const result = <rect x="0" y="0" width="100" height="50" />;
7
+ expect(result).toBe('<rect x="0" y="0" width="100" height="50"/>');
8
+ });
9
+
10
+ it("renders self-closing tag (circle)", () => {
11
+ const result = <circle cx="50" cy="50" r="25" />;
12
+ expect(result).toBe('<circle cx="50" cy="50" r="25"/>');
13
+ });
14
+
15
+ it("renders non-self-closing tag with children", () => {
16
+ const result = (
17
+ <text x="10" y="20">
18
+ hello
19
+ </text>
20
+ );
21
+ expect(result).toBe('<text x="10" y="20">hello</text>');
22
+ });
23
+
24
+ it("maps className to class", () => {
25
+ const result = <text className="t t-label">test</text>;
26
+ expect(result).toContain('class="t t-label"');
27
+ expect(result).not.toContain("className");
28
+ });
29
+
30
+ it("filters null and false props", () => {
31
+ const result = (
32
+ <text x="10" y={null} data-hidden={false}>
33
+ ok
34
+ </text>
35
+ );
36
+ expect(result).toContain('x="10"');
37
+ expect(result).not.toContain("y=");
38
+ expect(result).not.toContain("data-hidden");
39
+ });
40
+
41
+ it("escapes attribute values", () => {
42
+ const result = <text title={'he said "hi" & <bye>'}>ok</text>;
43
+ expect(result).toContain("&amp;");
44
+ expect(result).toContain("&quot;");
45
+ expect(result).toContain("&lt;");
46
+ expect(result).toContain("&gt;");
47
+ });
48
+
49
+ it("renders self-closing tag with content as non-self-closing", () => {
50
+ const result = h("rect", null, "inner");
51
+ expect(result).toBe("<rect>inner</rect>");
52
+ });
53
+ });
54
+
55
+ describe("h – function tags", () => {
56
+ it("calls component function with props and children", () => {
57
+ function MyComponent(props: { color: string; children?: unknown[] }) {
58
+ return `<g fill="${props.color}">${(props.children || []).join("")}</g>`;
59
+ }
60
+ const result = <MyComponent color="red">child</MyComponent>;
61
+ expect(result).toBe('<g fill="red">child</g>');
62
+ });
63
+ });
64
+
65
+ describe("Fragment", () => {
66
+ it("joins children", () => {
67
+ const result = (
68
+ <>
69
+ <rect x="0" y="0" width="10" height="10" />
70
+ <circle cx="5" cy="5" r="5" />
71
+ </>
72
+ );
73
+ expect(result).toContain("<rect");
74
+ expect(result).toContain("<circle");
75
+ });
76
+
77
+ it("filters falsy children", () => {
78
+ const result = Fragment({ children: ["hello", null, false, "world"] });
79
+ expect(result).toBe("helloworld");
80
+ });
81
+
82
+ it("returns empty string for no children", () => {
83
+ const result = Fragment({});
84
+ expect(result).toBe("");
85
+ });
86
+ });
@@ -0,0 +1,46 @@
1
+ const SELF_CLOSING = new Set([
2
+ "circle",
3
+ "rect",
4
+ "line",
5
+ "path",
6
+ "ellipse",
7
+ "polygon",
8
+ "polyline",
9
+ "use",
10
+ ]);
11
+
12
+ const escapeAttr = (s: string): string =>
13
+ s
14
+ .replace(/&/g, "&amp;")
15
+ .replace(/"/g, "&quot;")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;");
18
+
19
+ export function h(
20
+ tag: string | ((props: Record<string, unknown>) => string),
21
+ props: Record<string, unknown> | null,
22
+ ...children: unknown[]
23
+ ): string {
24
+ if (typeof tag === "function")
25
+ return tag({ ...props, children: children.flat() });
26
+
27
+ const attrs = Object.entries(props || {})
28
+ .filter(([, v]) => v != null && v !== false)
29
+ .map(([k, v]) => {
30
+ const name = k === "className" ? "class" : k;
31
+ return ` ${name}="${escapeAttr(String(v))}"`;
32
+ })
33
+ .join("");
34
+
35
+ const content = children
36
+ .flat()
37
+ .filter((c) => c != null && c !== false)
38
+ .join("");
39
+
40
+ if (SELF_CLOSING.has(tag) && !content) return `<${tag}${attrs}/>`;
41
+ return `<${tag}${attrs}>${content}</${tag}>`;
42
+ }
43
+
44
+ export function Fragment({ children }: { children?: unknown[] }): string {
45
+ return (children || []).flat().filter(Boolean).join("");
46
+ }
package/src/jsx.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ declare namespace JSX {
2
+ type Element = string;
3
+ interface IntrinsicElements {
4
+ [elemName: string]: Record<string, unknown>;
5
+ }
6
+ }