@the-forge-flow/visual-explainer-pi 0.1.1
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/LICENSE +21 -0
- package/README.md +226 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +238 -0
- package/dist/templates/architecture.d.ts +33 -0
- package/dist/templates/architecture.d.ts.map +1 -0
- package/dist/templates/architecture.js +292 -0
- package/dist/templates/data-table.d.ts +25 -0
- package/dist/templates/data-table.d.ts.map +1 -0
- package/dist/templates/data-table.js +255 -0
- package/dist/templates/mermaid.d.ts +11 -0
- package/dist/templates/mermaid.d.ts.map +1 -0
- package/dist/templates/mermaid.js +149 -0
- package/dist/templates/shared.d.ts +17 -0
- package/dist/templates/shared.d.ts.map +1 -0
- package/dist/templates/shared.js +663 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/utils/browser-open.d.ts +6 -0
- package/dist/utils/browser-open.d.ts.map +1 -0
- package/dist/utils/browser-open.js +33 -0
- package/dist/utils/file-writer.d.ts +8 -0
- package/dist/utils/file-writer.d.ts.map +1 -0
- package/dist/utils/file-writer.js +40 -0
- package/dist/utils/validators.d.ts +11 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +93 -0
- package/package.json +73 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for visual-explainer-pi
|
|
3
|
+
*/
|
|
4
|
+
export type VisualType = "architecture" | "flowchart" | "sequence" | "er" | "state" | "table" | "diff" | "plan" | "timeline" | "dashboard" | "slides" | "mermaid_custom";
|
|
5
|
+
export type Aesthetic = "blueprint" | "editorial" | "paper" | "terminal" | "dracula" | "nord" | "solarized" | "gruvbox";
|
|
6
|
+
export type Theme = "light" | "dark" | "auto";
|
|
7
|
+
export interface GenerateVisualParams {
|
|
8
|
+
type: VisualType;
|
|
9
|
+
content: string | Record<string, unknown>[];
|
|
10
|
+
title: string;
|
|
11
|
+
aesthetic?: Aesthetic;
|
|
12
|
+
theme?: Theme;
|
|
13
|
+
filename?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface GenerateResult {
|
|
16
|
+
filePath: string;
|
|
17
|
+
previewSnippet: string;
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ExtensionState {
|
|
21
|
+
recentFiles: string[];
|
|
22
|
+
tempDirs: string[];
|
|
23
|
+
defaultAesthetic: Aesthetic;
|
|
24
|
+
defaultTheme: Theme;
|
|
25
|
+
}
|
|
26
|
+
export interface TemplateContext {
|
|
27
|
+
title: string;
|
|
28
|
+
content: unknown;
|
|
29
|
+
aesthetic: Aesthetic;
|
|
30
|
+
theme: Theme;
|
|
31
|
+
}
|
|
32
|
+
export interface Palette {
|
|
33
|
+
[key: string]: string;
|
|
34
|
+
bg: string;
|
|
35
|
+
surface: string;
|
|
36
|
+
surface2: string;
|
|
37
|
+
surfaceElevated: string;
|
|
38
|
+
border: string;
|
|
39
|
+
borderBright: string;
|
|
40
|
+
text: string;
|
|
41
|
+
textDim: string;
|
|
42
|
+
accent: string;
|
|
43
|
+
accentDim: string;
|
|
44
|
+
green: string;
|
|
45
|
+
greenDim: string;
|
|
46
|
+
orange: string;
|
|
47
|
+
orangeDim: string;
|
|
48
|
+
teal: string;
|
|
49
|
+
tealDim: string;
|
|
50
|
+
plum: string;
|
|
51
|
+
plumDim: string;
|
|
52
|
+
}
|
|
53
|
+
export type FontPairing = {
|
|
54
|
+
body: string;
|
|
55
|
+
mono: string;
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,UAAU,GACnB,cAAc,GACd,WAAW,GACX,UAAU,GACV,IAAI,GACJ,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,UAAU,GACV,WAAW,GACX,QAAQ,GACR,gBAAgB,CAAC;AAEpB,MAAM,MAAM,SAAS,GAClB,WAAW,GACX,WAAW,GACX,OAAO,GACP,UAAU,GACV,SAAS,GACT,MAAM,GACN,WAAW,GACX,SAAS,CAAC;AAEb,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,SAAS,CAAC;IAC5B,YAAY,EAAE,KAAK,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC;IACrB,KAAK,EAAE,KAAK,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-open.d.ts","sourceRoot":"","sources":["../../src/utils/browser-open.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAElE,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCrF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform browser opening utility
|
|
3
|
+
*/
|
|
4
|
+
export async function openInBrowser(filePath, pi) {
|
|
5
|
+
const platform = process.platform;
|
|
6
|
+
let command;
|
|
7
|
+
let args;
|
|
8
|
+
switch (platform) {
|
|
9
|
+
case "darwin":
|
|
10
|
+
command = "open";
|
|
11
|
+
args = [filePath];
|
|
12
|
+
break;
|
|
13
|
+
case "linux":
|
|
14
|
+
command = "xdg-open";
|
|
15
|
+
args = [filePath];
|
|
16
|
+
break;
|
|
17
|
+
case "win32":
|
|
18
|
+
command = "cmd";
|
|
19
|
+
args = ["/c", "start", "", filePath];
|
|
20
|
+
break;
|
|
21
|
+
default:
|
|
22
|
+
throw new Error(`Unsupported platform: ${platform}. Please open manually: ${filePath}`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const result = await pi.exec(command, args, { timeout: 10000 });
|
|
26
|
+
if (result.code !== 0) {
|
|
27
|
+
throw new Error(`Browser open failed: ${result.stderr || "Unknown error"}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new Error(`Failed to open browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File writing utilities for visual-explainer
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionState } from "../types.js";
|
|
5
|
+
export declare function ensureOutputDir(): Promise<string>;
|
|
6
|
+
export declare function writeHtmlFile(filename: string, html: string, state: ExtensionState): Promise<string>;
|
|
7
|
+
export declare function createInitialState(): ExtensionState;
|
|
8
|
+
//# sourceMappingURL=file-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-writer.d.ts","sourceRoot":"","sources":["../../src/utils/file-writer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAOvD;AAED,wBAAsB,aAAa,CAClC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,cAAc,GACnB,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED,wBAAgB,kBAAkB,IAAI,cAAc,CAOnD"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File writing utilities for visual-explainer
|
|
3
|
+
*/
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
const OUTPUT_DIR = join(homedir(), ".agent", "diagrams");
|
|
8
|
+
export async function ensureOutputDir() {
|
|
9
|
+
try {
|
|
10
|
+
await mkdir(OUTPUT_DIR, { recursive: true });
|
|
11
|
+
return OUTPUT_DIR;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
throw new Error(`Failed to create output directory: ${OUTPUT_DIR}: ${error}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function writeHtmlFile(filename, html, state) {
|
|
18
|
+
const outputDir = await ensureOutputDir();
|
|
19
|
+
const filePath = join(outputDir, filename);
|
|
20
|
+
try {
|
|
21
|
+
await writeFile(filePath, html, "utf8");
|
|
22
|
+
// Track in recent files
|
|
23
|
+
state.recentFiles.unshift(filePath);
|
|
24
|
+
if (state.recentFiles.length > 10) {
|
|
25
|
+
state.recentFiles = state.recentFiles.slice(0, 10);
|
|
26
|
+
}
|
|
27
|
+
return filePath;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Failed to write HTML file: ${error}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function createInitialState() {
|
|
34
|
+
return {
|
|
35
|
+
recentFiles: [],
|
|
36
|
+
tempDirs: [],
|
|
37
|
+
defaultAesthetic: "blueprint",
|
|
38
|
+
defaultTheme: "auto",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utilities for visual-explainer
|
|
3
|
+
*/
|
|
4
|
+
import type { Aesthetic, GenerateVisualParams, Theme, VisualType } from "../types.js";
|
|
5
|
+
export declare function validateVisualType(type: unknown): VisualType;
|
|
6
|
+
export declare function validateAesthetic(aesthetic: unknown): Aesthetic;
|
|
7
|
+
export declare function validateTheme(theme: unknown): Theme;
|
|
8
|
+
export declare function validateParams(params: unknown): GenerateVisualParams;
|
|
9
|
+
export declare function sanitizeFilename(filename: string): string;
|
|
10
|
+
export declare function generateDefaultFilename(title: string): string;
|
|
11
|
+
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../src/utils/validators.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA8BtF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,CAO5D;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,CAO/D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAOnD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,oBAAoB,CA4BpE;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQzD;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO7D"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utilities for visual-explainer
|
|
3
|
+
*/
|
|
4
|
+
const VALID_VISUAL_TYPES = [
|
|
5
|
+
"architecture",
|
|
6
|
+
"flowchart",
|
|
7
|
+
"sequence",
|
|
8
|
+
"er",
|
|
9
|
+
"state",
|
|
10
|
+
"table",
|
|
11
|
+
"diff",
|
|
12
|
+
"plan",
|
|
13
|
+
"timeline",
|
|
14
|
+
"dashboard",
|
|
15
|
+
"slides",
|
|
16
|
+
"mermaid_custom",
|
|
17
|
+
];
|
|
18
|
+
const VALID_AESTHETICS = [
|
|
19
|
+
"blueprint",
|
|
20
|
+
"editorial",
|
|
21
|
+
"paper",
|
|
22
|
+
"terminal",
|
|
23
|
+
"dracula",
|
|
24
|
+
"nord",
|
|
25
|
+
"solarized",
|
|
26
|
+
"gruvbox",
|
|
27
|
+
];
|
|
28
|
+
const VALID_THEMES = ["light", "dark", "auto"];
|
|
29
|
+
export function validateVisualType(type) {
|
|
30
|
+
if (typeof type !== "string" || !VALID_VISUAL_TYPES.includes(type)) {
|
|
31
|
+
throw new Error(`Invalid visual type: ${type}. Must be one of: ${VALID_VISUAL_TYPES.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
return type;
|
|
34
|
+
}
|
|
35
|
+
export function validateAesthetic(aesthetic) {
|
|
36
|
+
if (aesthetic === undefined)
|
|
37
|
+
return "blueprint";
|
|
38
|
+
if (typeof aesthetic !== "string" || !VALID_AESTHETICS.includes(aesthetic)) {
|
|
39
|
+
console.warn(`Invalid aesthetic: ${aesthetic}. Falling back to "blueprint"`);
|
|
40
|
+
return "blueprint";
|
|
41
|
+
}
|
|
42
|
+
return aesthetic;
|
|
43
|
+
}
|
|
44
|
+
export function validateTheme(theme) {
|
|
45
|
+
if (theme === undefined)
|
|
46
|
+
return "auto";
|
|
47
|
+
if (typeof theme !== "string" || !VALID_THEMES.includes(theme)) {
|
|
48
|
+
console.warn(`Invalid theme: ${theme}. Falling back to "auto"`);
|
|
49
|
+
return "auto";
|
|
50
|
+
}
|
|
51
|
+
return theme;
|
|
52
|
+
}
|
|
53
|
+
export function validateParams(params) {
|
|
54
|
+
if (!params || typeof params !== "object") {
|
|
55
|
+
throw new Error("Parameters must be an object");
|
|
56
|
+
}
|
|
57
|
+
const p = params;
|
|
58
|
+
// Required fields
|
|
59
|
+
if (!p.title || typeof p.title !== "string") {
|
|
60
|
+
throw new Error("title is required and must be a string");
|
|
61
|
+
}
|
|
62
|
+
if (!p.content) {
|
|
63
|
+
throw new Error("content is required");
|
|
64
|
+
}
|
|
65
|
+
const type = validateVisualType(p.type);
|
|
66
|
+
const aesthetic = validateAesthetic(p.aesthetic);
|
|
67
|
+
const theme = validateTheme(p.theme);
|
|
68
|
+
return {
|
|
69
|
+
type,
|
|
70
|
+
content: p.content,
|
|
71
|
+
title: p.title,
|
|
72
|
+
aesthetic,
|
|
73
|
+
theme,
|
|
74
|
+
filename: p.filename && typeof p.filename === "string" ? p.filename : undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function sanitizeFilename(filename) {
|
|
78
|
+
// Remove any path components and sanitize
|
|
79
|
+
const base = filename.replace(/^[./\\]+/, "").replace(/[\\/:*?"<>|]/g, "-");
|
|
80
|
+
// Ensure .html extension
|
|
81
|
+
if (!base.endsWith(".html")) {
|
|
82
|
+
return `${base}.html`;
|
|
83
|
+
}
|
|
84
|
+
return base;
|
|
85
|
+
}
|
|
86
|
+
export function generateDefaultFilename(title) {
|
|
87
|
+
const timestamp = new Date().toISOString().split("T")[0];
|
|
88
|
+
const sanitized = title
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
91
|
+
.replace(/^-+|-+$/g, "");
|
|
92
|
+
return `${timestamp}-${sanitized || "visual"}.html`;
|
|
93
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@the-forge-flow/visual-explainer-pi",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "PI extension for generating beautiful HTML visualizations of diagrams, architecture, and data",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.build.json",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"test:coverage": "vitest run --coverage",
|
|
18
|
+
"lint": "biome lint .",
|
|
19
|
+
"lint:fix": "biome lint . --write",
|
|
20
|
+
"format": "biome format .",
|
|
21
|
+
"format:fix": "biome format . --write",
|
|
22
|
+
"check": "biome check .",
|
|
23
|
+
"check:fix": "biome check . --write",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepare": "lefthook install || true"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "^1.9.4",
|
|
29
|
+
"@commitlint/cli": "^19.0.0",
|
|
30
|
+
"@commitlint/config-conventional": "^19.0.0",
|
|
31
|
+
"@sinclair/typebox": "^0.34.0",
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"lefthook": "^2.1.5",
|
|
34
|
+
"typescript": "^5.7.0",
|
|
35
|
+
"vitest": "^2.1.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
39
|
+
"@mariozechner/pi-ai": "*",
|
|
40
|
+
"@mariozechner/pi-tui": "*",
|
|
41
|
+
"@sinclair/typebox": "*"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20.0.0"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"pi-package",
|
|
48
|
+
"pi",
|
|
49
|
+
"extension",
|
|
50
|
+
"visualization",
|
|
51
|
+
"diagrams",
|
|
52
|
+
"architecture",
|
|
53
|
+
"html",
|
|
54
|
+
"mermaid"
|
|
55
|
+
],
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/monsieurbarti/visual-explainer-pi.git"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/monsieurbarti/visual-explainer-pi/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/monsieurbarti/visual-explainer-pi#readme",
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
|
+
"pi": {
|
|
69
|
+
"extensions": [
|
|
70
|
+
"./dist/index.js"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|