@wkovacs64/add-icon 0.1.0-dev.8525808f

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.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Justin R. Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # @wkovacs64/add-icon
2
+
3
+ A command-line tool to download icons from the [Iconify Framework](https://iconify.design/) and
4
+ apply custom transformations.
5
+
6
+ ## Installation
7
+
8
+ Add it to your project:
9
+
10
+ ```bash
11
+ npm install @wkovacs64/add-icon
12
+ ```
13
+
14
+ Or use it directly with npx without installing:
15
+
16
+ ```bash
17
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Basic Usage
23
+
24
+ Download an icon:
25
+
26
+ ```bash
27
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle
28
+ ```
29
+
30
+ Specify an output directory:
31
+
32
+ ```bash
33
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --output-dir ./my-icons
34
+ ```
35
+
36
+ ### Transformations
37
+
38
+ Apply built-in transformations:
39
+
40
+ ```bash
41
+ # Remove width and height attributes
42
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --remove-size
43
+
44
+ # Optimize SVG with SVGO
45
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --optimize
46
+
47
+ # Minify SVG
48
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --minify
49
+
50
+ # Apply multiple transformations
51
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --remove-size --optimize --minify
52
+ ```
53
+
54
+ ### Custom Transformations
55
+
56
+ You can write custom transforms in either JavaScript or TypeScript!
57
+
58
+ #### JavaScript Transform
59
+
60
+ Create a custom transform file (e.g., `my-transform.js`):
61
+
62
+ ```js
63
+ /**
64
+ * Custom transform to add a title element to SVG
65
+ * @param {Object} args - Transform arguments
66
+ * @param {string} args.svg - SVG content
67
+ * @param {string} args.iconName - Icon name (e.g., 'heroicons:arrow-up-circle')
68
+ * @param {string} args.prefix - Icon set prefix (e.g., 'heroicons')
69
+ * @param {string} args.name - Icon name without prefix (e.g., 'arrow-up-circle')
70
+ * @returns {string} - Transformed SVG
71
+ */
72
+ export default function addTitle(args) {
73
+ const titleElement = `<title>${args.iconName}</title>`;
74
+ return args.svg.replace(/<svg([^>]*)>/, `<svg$1>${titleElement}`);
75
+ }
76
+ ```
77
+
78
+ #### TypeScript Transform
79
+
80
+ Create a custom transform file (e.g., `my-transform.ts`):
81
+
82
+ ```ts
83
+ import type { TransformArgs } from '@wkovacs64/add-icon';
84
+
85
+ /**
86
+ * Custom transform to add a title element to SVG
87
+ * @param args - Transform arguments containing SVG content and icon information
88
+ * @returns The transformed SVG
89
+ */
90
+ export default function addTitle(args: TransformArgs): string {
91
+ const titleElement = `<title>${args.iconSet}:${args.iconName}</title>`;
92
+ return args.svg.replace(/<svg([^>]*)>/, `<svg$1>${titleElement}`);
93
+ }
94
+ ```
95
+
96
+ Then use it with the CLI:
97
+
98
+ ```bash
99
+ # JavaScript transform
100
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --transform ./my-transform.js
101
+
102
+ # TypeScript transform
103
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --transform ./my-transform.ts
104
+ ```
105
+
106
+ ## Configuration File
107
+
108
+ You can create a configuration file (`add-icon.config.js`) in your project root:
109
+
110
+ ```js
111
+ import { transforms } from '@wkovacs64/add-icon';
112
+
113
+ // Define custom transform
114
+ function addCustomAttribute(args) {
115
+ return args.svg.replace(/<svg/, `<svg data-icon="${args.iconName}"`);
116
+ }
117
+
118
+ export default {
119
+ outputDir: './assets/icons',
120
+ transforms: [transforms.removeSize, transforms.optimizeSvg, addCustomAttribute],
121
+ };
122
+ ```
123
+
124
+ ## Using as a Library
125
+
126
+ You can also use iconify-cli as a library in your own projects:
127
+
128
+ ### JavaScript
129
+
130
+ ```js
131
+ import { downloadIcon, transforms } from '@wkovacs64/add-icon';
132
+
133
+ // Create custom transform
134
+ function addCustomAttribute(args) {
135
+ return args.svg.replace(/<svg/, `<svg data-custom="${args.iconSet}"`);
136
+ }
137
+
138
+ // Download an icon with transforms
139
+ async function downloadCustomIcon() {
140
+ const iconPath = await downloadIcon('heroicons:heart', {
141
+ outputDir: './icons',
142
+ transforms: [transforms.removeSize, transforms.optimizeSvg, addCustomAttribute],
143
+ });
144
+
145
+ console.log(`Icon saved to: ${iconPath}`);
146
+ }
147
+
148
+ downloadCustomIcon();
149
+ ```
150
+
151
+ ### TypeScript
152
+
153
+ ```ts
154
+ import { downloadIcon, transforms, type TransformArgs } from '@wkovacs64/add-icon';
155
+
156
+ // Create custom transform
157
+ const addCustomAttribute = (args: TransformArgs): string => {
158
+ return args.svg.replace(/<svg/, `<svg data-custom="${args.iconSet}"`);
159
+ };
160
+
161
+ // Download an icon with transforms
162
+ async function downloadCustomIcon(): Promise<void> {
163
+ try {
164
+ const iconPath = await downloadIcon('heroicons:heart', {
165
+ outputDir: './icons',
166
+ transforms: [transforms.removeSize, transforms.optimizeSvg, addCustomAttribute],
167
+ });
168
+
169
+ console.log(`Icon saved to: ${iconPath}`);
170
+ } catch (error) {
171
+ console.error(
172
+ 'Error downloading icon:',
173
+ error instanceof Error ? error.message : String(error),
174
+ );
175
+ }
176
+ }
177
+
178
+ downloadCustomIcon();
179
+ ```
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,11 @@
1
+ import type { IconifyConfig } from './types.js';
2
+ /**
3
+ * Default configuration
4
+ */
5
+ export declare const defaultConfig: IconifyConfig;
6
+ /**
7
+ * Loads configuration from file if it exists
8
+ * @param configPath - Path to config file
9
+ * @returns Configuration object
10
+ */
11
+ export declare function loadConfig(configPath?: string): Promise<IconifyConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Default configuration
5
+ */
6
+ export const defaultConfig = {
7
+ outputDir: './icons',
8
+ };
9
+ /**
10
+ * Loads configuration from file if it exists
11
+ * @param configPath - Path to config file
12
+ * @returns Configuration object
13
+ */
14
+ export async function loadConfig(configPath) {
15
+ // Use provided config path or look for default config file
16
+ const configFile = configPath || path.resolve(process.cwd(), 'add-icon.config.js');
17
+ try {
18
+ if (fs.existsSync(configFile)) {
19
+ // For ESM, we need to use dynamic import with file:// protocol
20
+ const fileUrl = `file://${configFile}`;
21
+ const config = await import(fileUrl);
22
+ return { ...defaultConfig, ...config.default };
23
+ }
24
+ }
25
+ catch (error) {
26
+ const errorMessage = error instanceof Error ? error.message : String(error);
27
+ console.error(`Error loading config file: ${errorMessage}`);
28
+ }
29
+ return defaultConfig;
30
+ }
@@ -0,0 +1,17 @@
1
+ import type { IconifyConfig } from './types.js';
2
+ /**
3
+ * Parses an icon reference into iconSet and iconName
4
+ * @param iconReference - Reference in format 'iconSet:iconName'
5
+ * @returns Object with iconSet and iconName
6
+ */
7
+ export declare function parseIconReference(iconReference: string): {
8
+ iconSet: string;
9
+ iconName: string;
10
+ };
11
+ /**
12
+ * Downloads an icon and applies transforms
13
+ * @param iconReference - Icon reference (e.g., 'heroicons:arrow-up-circle')
14
+ * @param config - Configuration options
15
+ * @returns Path to saved icon file
16
+ */
17
+ export declare function downloadIcon(iconReference: string, config: IconifyConfig): Promise<string>;
@@ -0,0 +1,88 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { iconToSVG } from '@iconify/utils';
4
+ /**
5
+ * Parses an icon reference into iconSet and iconName
6
+ * @param iconReference - Reference in format 'iconSet:iconName'
7
+ * @returns Object with iconSet and iconName
8
+ */
9
+ export function parseIconReference(iconReference) {
10
+ const parts = iconReference.split(':');
11
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
12
+ throw new Error(`Invalid icon reference: ${iconReference}. Expected format: iconSet:iconName`);
13
+ }
14
+ return {
15
+ iconSet: parts[0],
16
+ iconName: parts[1],
17
+ };
18
+ }
19
+ /**
20
+ * Fetches icon data from Iconify API
21
+ * @param iconSet - Icon set name
22
+ * @param iconName - Icon name
23
+ * @returns Promise with icon data
24
+ */
25
+ async function fetchIconData(iconSet, iconName) {
26
+ const apiUrl = `https://api.iconify.design/${iconSet}.json?icons=${iconName}`;
27
+ try {
28
+ const response = await fetch(apiUrl);
29
+ if (!response.ok) {
30
+ throw new Error(`HTTP error! Status: ${response.status}`);
31
+ }
32
+ const data = await response.json();
33
+ if (!data || !data.icons || !data.icons[iconName]) {
34
+ throw new Error(`Icon '${iconName}' not found in '${iconSet}' icon set`);
35
+ }
36
+ return data.icons[iconName];
37
+ }
38
+ catch (error) {
39
+ const errorMessage = error instanceof Error ? error.message : String(error);
40
+ throw new Error(`Failed to fetch icon data: ${errorMessage}`);
41
+ }
42
+ }
43
+ /**
44
+ * Downloads an icon and applies transforms
45
+ * @param iconReference - Icon reference (e.g., 'heroicons:arrow-up-circle')
46
+ * @param config - Configuration options
47
+ * @returns Path to saved icon file
48
+ */
49
+ export async function downloadIcon(iconReference, config) {
50
+ try {
51
+ const { iconSet, iconName } = parseIconReference(iconReference);
52
+ // Ensure the output directory exists
53
+ if (!fs.existsSync(config.outputDir)) {
54
+ fs.mkdirSync(config.outputDir, { recursive: true });
55
+ }
56
+ // Load the icon data
57
+ const iconData = await fetchIconData(iconSet, iconName);
58
+ // Convert icon data to SVG
59
+ const renderData = iconToSVG(iconData, {
60
+ height: 'auto',
61
+ });
62
+ // Create SVG string
63
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${renderData.attributes.width}" height="${renderData.attributes.height}" viewBox="${renderData.attributes.viewBox}">${renderData.body}</svg>`;
64
+ // Apply transforms if specified
65
+ if (config.transforms && config.transforms.length > 0) {
66
+ for (const transform of config.transforms) {
67
+ // Create transform arguments object
68
+ const transformArgs = {
69
+ svg,
70
+ iconSet,
71
+ iconName,
72
+ };
73
+ // Apply transform
74
+ svg = await Promise.resolve(transform(transformArgs));
75
+ }
76
+ }
77
+ // Create file name
78
+ const fileName = `${iconSet}-${iconName}.svg`;
79
+ const filePath = path.join(config.outputDir, fileName);
80
+ // Write the SVG file
81
+ fs.writeFileSync(filePath, svg, 'utf8');
82
+ return filePath;
83
+ }
84
+ catch (error) {
85
+ const errorMessage = error instanceof Error ? error.message : String(error);
86
+ throw new Error(`Failed to download icon: ${errorMessage}`);
87
+ }
88
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import type { IconTransform, TransformArgs } from './types.js';
3
+ import * as defaultTransforms from './transforms.js';
4
+ export type { IconTransform, TransformArgs };
5
+ export { defaultTransforms as transforms };
6
+ export { downloadIcon, parseIconReference } from './iconify.js';
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import url from 'node:url';
5
+ import os from 'node:os';
6
+ import { execSync } from 'node:child_process';
7
+ import { Command } from 'commander';
8
+ import { downloadIcon } from './iconify.js';
9
+ import { loadConfig } from './config.js';
10
+ import * as defaultTransforms from './transforms.js';
11
+ // Re-export transforms for easy importing by users
12
+ export { defaultTransforms as transforms };
13
+ // Re-export other useful functions
14
+ export { downloadIcon, parseIconReference } from './iconify.js';
15
+ // Create CLI program
16
+ const program = new Command();
17
+ program
18
+ .name('add-icon')
19
+ .description('Download and transform icons from Iconify')
20
+ .version('1.0.0')
21
+ .argument('<icon>', 'Icon reference (e.g., heroicons:arrow-up-circle)')
22
+ .option('-o, --output-dir <dir>', 'Directory to save icon')
23
+ .option('-c, --config <path>', 'Path to config file')
24
+ .option('--remove-size', 'Remove width and height attributes')
25
+ .option('--optimize', 'Optimize SVG with SVGO')
26
+ .option('--minify', 'Minify SVG by removing whitespace')
27
+ .option('-t, --transform <path>', 'Path to custom transform module (.js or .ts)')
28
+ .action(async (icon, options) => {
29
+ try {
30
+ // Load config (first from config file, then override with CLI options)
31
+ const config = await loadConfig(options.config);
32
+ // Override output directory if specified in CLI
33
+ if (options.outputDir) {
34
+ config.outputDir = options.outputDir;
35
+ }
36
+ // Prepare transforms array
37
+ const transforms = [];
38
+ // Add requested built-in transforms
39
+ if (options.removeSize) {
40
+ transforms.push(defaultTransforms.removeSize);
41
+ }
42
+ if (options.optimize) {
43
+ transforms.push(defaultTransforms.optimizeSvg);
44
+ }
45
+ if (options.minify) {
46
+ transforms.push(defaultTransforms.minifySvg);
47
+ }
48
+ // Load custom transform if specified
49
+ if (options.transform) {
50
+ try {
51
+ const transformPath = path.resolve(process.cwd(), options.transform);
52
+ let customTransform;
53
+ // Handle TypeScript files
54
+ if (transformPath.endsWith('.ts')) {
55
+ // Create a temporary JS file for the transform
56
+ const jsPath = transformPath.replace(/\.ts$/, '.js');
57
+ try {
58
+ // Use tsc to compile the TypeScript file
59
+ execSync(`npx tsc "${transformPath}" --outDir "${path.dirname(transformPath)}" --target es2020 --module NodeNext --moduleResolution NodeNext --esModuleInterop`);
60
+ // Import the compiled JS file
61
+ customTransform = await import(`file://${jsPath}`);
62
+ // Clean up temporary JS file if not in dev mode
63
+ if (process.env.NODE_ENV !== 'development') {
64
+ fs.unlinkSync(jsPath);
65
+ }
66
+ }
67
+ catch (err) {
68
+ const errorMessage = err instanceof Error ? err.message : String(err);
69
+ console.error(`Error transpiling TypeScript transform: ${errorMessage}`);
70
+ console.error('Make sure TypeScript is installed or use a JavaScript (.js) transform file.');
71
+ process.exit(1);
72
+ }
73
+ }
74
+ else {
75
+ // For JavaScript files, use dynamic import
76
+ customTransform = await import(`file://${transformPath}`);
77
+ }
78
+ if (customTransform && typeof customTransform.default === 'function') {
79
+ transforms.push(customTransform.default);
80
+ }
81
+ else {
82
+ console.error('Custom transform must export a default function');
83
+ process.exit(1);
84
+ }
85
+ }
86
+ catch (error) {
87
+ const errorMessage = error instanceof Error ? error.message : String(error);
88
+ console.error(`Failed to load custom transform: ${errorMessage}`);
89
+ process.exit(1);
90
+ }
91
+ }
92
+ // Add transforms to config
93
+ if (transforms.length > 0) {
94
+ config.transforms = transforms;
95
+ }
96
+ // Download the icon
97
+ console.log(`Downloading icon: ${icon}...`);
98
+ const savedPath = await downloadIcon(icon, config);
99
+ console.log(`✓ Icon saved to: ${savedPath}`);
100
+ }
101
+ catch (error) {
102
+ const errorMessage = error instanceof Error ? error.message : String(error);
103
+ console.error(`Error: ${errorMessage}`);
104
+ process.exit(1);
105
+ }
106
+ });
107
+ const __filename = url.fileURLToPath(import.meta.url);
108
+ const __dirname = path.dirname(__filename);
109
+ // This logic only runs when executed directly as CLI, not when imported as a library
110
+ if (os.platform() === 'win32'
111
+ ? process.argv[1] === __filename
112
+ : process.argv[1] === __filename || process.argv[1] === __dirname) {
113
+ // Parse command line arguments
114
+ program.parse();
115
+ }
@@ -0,0 +1,13 @@
1
+ import type { IconTransform } from './types.js';
2
+ /**
3
+ * Removes width and height attributes from SVG
4
+ */
5
+ export declare const removeSize: IconTransform;
6
+ /**
7
+ * Optimizes SVG with SVGO
8
+ */
9
+ export declare const optimizeSvg: IconTransform;
10
+ /**
11
+ * Minifies SVG by removing whitespace
12
+ */
13
+ export declare const minifySvg: IconTransform;
@@ -0,0 +1,37 @@
1
+ import { optimize } from 'svgo';
2
+ /**
3
+ * Removes width and height attributes from SVG
4
+ */
5
+ export const removeSize = (args) => {
6
+ const { svg } = args;
7
+ return svg.replace(/\s+width="[^"]+"/g, '').replace(/\s+height="[^"]+"/g, '');
8
+ };
9
+ /**
10
+ * Optimizes SVG with SVGO
11
+ */
12
+ export const optimizeSvg = (args) => {
13
+ const { svg } = args;
14
+ const result = optimize(svg, {
15
+ plugins: [
16
+ 'preset-default',
17
+ 'removeXMLNS',
18
+ {
19
+ name: 'removeAttrs',
20
+ params: {
21
+ attrs: '(data-name)',
22
+ },
23
+ },
24
+ ],
25
+ });
26
+ return result.data;
27
+ };
28
+ /**
29
+ * Minifies SVG by removing whitespace
30
+ */
31
+ export const minifySvg = (args) => {
32
+ const { svg } = args;
33
+ return svg
34
+ .replace(/>[\s\r\n]+</g, '><') // Remove whitespace between tags
35
+ .replace(/\s{2,}/g, ' ') // Reduce multiple spaces to single space
36
+ .trim();
37
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Arguments passed to transform functions
3
+ */
4
+ export interface TransformArgs {
5
+ /** The SVG content as a string */
6
+ svg: string;
7
+ /** The icon set (e.g., 'heroicons') */
8
+ iconSet: string;
9
+ /** The icon name (e.g., 'arrow-up-circle') */
10
+ iconName: string;
11
+ }
12
+ /**
13
+ * SVG transformation function type
14
+ */
15
+ export type IconTransform = (args: TransformArgs) => Promise<string> | string;
16
+ /**
17
+ * Configuration options for the Iconify CLI
18
+ */
19
+ export interface IconifyConfig {
20
+ /** Directory to output icons */
21
+ outputDir: string;
22
+ /** Array of transform functions to apply to icons */
23
+ transforms?: IconTransform[];
24
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@wkovacs64/add-icon",
3
+ "version": "0.1.0-dev.8525808f",
4
+ "description": "CLI tool to download and transform icons from Iconify",
5
+ "keywords": [
6
+ "iconify",
7
+ "icons",
8
+ "svg",
9
+ "cli",
10
+ "download"
11
+ ],
12
+ "author": "Justin R. Hall <justin.r.hall@gmail.com>",
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "main": "dist/index.js",
16
+ "exports": {
17
+ ".": "./dist/index.js",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "bin": {
21
+ "add-icon": "dist/index.js"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "clean": "del-cli dist",
29
+ "prebuild": "npm run --silent clean",
30
+ "prepublishOnly": "run-p --silent lint typecheck build",
31
+ "start": "node dist/index.js",
32
+ "dev": "tsx src/index.ts",
33
+ "typecheck": "attw --pack --profile esm-only",
34
+ "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
35
+ "format": "prettier --cache --write .",
36
+ "format:check": "prettier --cache --check .",
37
+ "changeset": "changeset",
38
+ "changeset:version": "changeset version && npm install --package-lock-only",
39
+ "changeset:publish": "changeset publish"
40
+ },
41
+ "prettier": "@wkovacs64/prettier-config",
42
+ "engines": {
43
+ "node": ">=20.19.0"
44
+ },
45
+ "dependencies": {
46
+ "@iconify/utils": "^2.3.0",
47
+ "commander": "^13.1.0",
48
+ "svgo": "^3.3.2",
49
+ "tsx": "^4.19.3",
50
+ "typescript": "^5.8.3"
51
+ },
52
+ "devDependencies": {
53
+ "@arethetypeswrong/cli": "0.17.4",
54
+ "@changesets/changelog-github": "0.5.1",
55
+ "@changesets/cli": "2.29.0",
56
+ "@types/node": "22.14.1",
57
+ "@wkovacs64/eslint-config": "7.5.2",
58
+ "@wkovacs64/prettier-config": "4.1.1",
59
+ "del-cli": "6.0.0",
60
+ "eslint": "9.24.0",
61
+ "npm-run-all2": "7.0.2",
62
+ "prettier": "3.5.3"
63
+ }
64
+ }