obsidian-plugin-config 1.5.12 → 1.6.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.
Files changed (43) hide show
  1. package/.vscode/tasks.json +5 -69
  2. package/DONE.md +60 -0
  3. package/FINAL_SUMMARY.md +163 -0
  4. package/README.md +70 -35
  5. package/bin/obsidian-inject.js +1 -1
  6. package/docs/APPLIED_MODIFS.md +104 -0
  7. package/docs/INTERACTIVE_INJECTION.md +137 -0
  8. package/docs/LLM-GUIDE.md +60 -50
  9. package/docs/TECHNICAL_NOTES.md +43 -0
  10. package/docs/implementation_plan.md +127 -0
  11. package/docs/modifs.md +434 -0
  12. package/package.json +3 -29
  13. package/scripts/acp.ts +7 -11
  14. package/scripts/build-npm.ts +78 -75
  15. package/scripts/help.ts +45 -107
  16. package/scripts/inject-core.ts +74 -36
  17. package/scripts/inject-options.ts +139 -0
  18. package/scripts/inject-path.ts +18 -4
  19. package/scripts/inject-prompt.ts +2 -1
  20. package/scripts/update-version-config.ts +2 -5
  21. package/templates/.gitattributes +4 -0
  22. package/templates/.github/workflows/release.yml +1 -1
  23. package/templates/.prettierignore +6 -0
  24. package/templates/.vscode/extensions.json +8 -0
  25. package/templates/.vscode/settings.json +0 -1
  26. package/templates/package.json +3 -2
  27. package/templates/scripts/esbuild.config.ts +16 -21
  28. package/tsconfig.json +2 -8
  29. package/.injection-info.json +0 -5
  30. package/docs/EXPORTS-EXPLAINED.md +0 -164
  31. package/manifest.json +0 -11
  32. package/scripts/esbuild.config.ts +0 -268
  33. package/scripts/sync-template-deps.ts +0 -59
  34. package/scripts/update-exports.js +0 -82
  35. package/src/index.ts +0 -6
  36. package/src/main.ts +0 -117
  37. package/src/modals/GenericConfirmModal.ts +0 -79
  38. package/src/modals/index.ts +0 -7
  39. package/src/tools/index.ts +0 -9
  40. package/src/utils/NoticeHelper.ts +0 -85
  41. package/src/utils/SettingsHelper.ts +0 -170
  42. package/src/utils/index.ts +0 -3
  43. package/versions.json +0 -64
@@ -1,268 +0,0 @@
1
- import esbuild from 'esbuild';
2
- import process from 'process';
3
- import builtins from 'builtin-modules';
4
- import { config } from 'dotenv';
5
- import path from 'path';
6
- import { readFileSync } from 'fs';
7
- import { rm } from 'fs/promises';
8
- import fs from 'fs';
9
- import {
10
- isValidPath,
11
- copyFilesToTargetDir,
12
- askQuestion,
13
- createReadlineInterface
14
- } from './utils.js';
15
-
16
- // Determine the plugin directory (where the script is called from)
17
- const pluginDir = process.cwd();
18
-
19
- // Create readline interface for prompts
20
- const rl = createReadlineInterface();
21
-
22
- async function promptForVaultPath(envKey: string): Promise<string> {
23
- const vaultType = envKey === 'REAL_VAULT' ? 'real' : 'test';
24
- const usage =
25
- envKey === 'REAL_VAULT'
26
- ? 'for final plugin installation'
27
- : 'for development and testing';
28
-
29
- console.log(`❓ ${envKey} path is required ${usage}`);
30
- const path = await askQuestion(
31
- `📝 Enter your ${vaultType} vault path (or Ctrl+C to cancel): `,
32
- rl
33
- );
34
-
35
- if (!path) {
36
- console.log('❌ No path provided, exiting...');
37
- process.exit(1);
38
- }
39
-
40
- return path;
41
- }
42
-
43
- async function updateEnvFile(envKey: string, vaultPath: string): Promise<void> {
44
- const envPath = path.join(pluginDir, '.env');
45
- let envContent = '';
46
-
47
- // Read existing .env if it exists
48
- try {
49
- envContent = readFileSync(envPath, 'utf8');
50
- } catch {
51
- // File doesn't exist, start with empty content
52
- }
53
-
54
- // Update or add the variable
55
- const regex = new RegExp(`^${envKey}=.*$`, 'm');
56
- const newLine = `${envKey}=${vaultPath}`;
57
-
58
- if (regex.test(envContent)) {
59
- envContent = envContent.replace(regex, newLine);
60
- } else {
61
- envContent += envContent.endsWith('\n') ? '' : '\n';
62
- envContent += newLine + '\n';
63
- }
64
-
65
- // Write back to .env
66
- await import('fs').then((fs) => fs.writeFileSync(envPath, envContent));
67
- console.log(`✅ Updated ${envKey} in .env file`);
68
- }
69
-
70
- function validateVaultPath(vaultPath: string): boolean {
71
- // Check if the path contains .obsidian directory
72
- const obsidianPath = path.join(vaultPath, '.obsidian');
73
- const pluginsPath = path.join(vaultPath, '.obsidian', 'plugins');
74
-
75
- return fs.existsSync(obsidianPath) && fs.existsSync(pluginsPath);
76
- }
77
-
78
- function getVaultPath(vaultPath: string): string {
79
- // Validate that this is a proper vault path
80
- if (!validateVaultPath(vaultPath)) {
81
- console.error(`❌ Invalid vault path: ${vaultPath}`);
82
- console.error(` The path must contain a .obsidian/plugins directory`);
83
- console.error(` Please enter a valid Obsidian vault path`);
84
- process.exit(1);
85
- }
86
-
87
- // Check if the path already contains the plugins directory path
88
- const pluginsPath = path.join('.obsidian', 'plugins');
89
- if (vaultPath.includes(pluginsPath)) {
90
- return path.join(vaultPath, manifest.id);
91
- } else {
92
- return path.join(vaultPath, '.obsidian', 'plugins', manifest.id);
93
- }
94
- }
95
- const manifestPath = path.join(pluginDir, 'manifest.json');
96
-
97
- // Check if manifest exists (for plugin-config itself, it might not exist)
98
- if (!fs.existsSync(manifestPath)) {
99
- console.log(
100
- '⚠️ No manifest.json found - this script is designed for Obsidian plugins'
101
- );
102
- console.log(" If you're building plugin-config itself, use 'tsc' instead");
103
- process.exit(0);
104
- }
105
-
106
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
107
-
108
- config();
109
-
110
- const EXTERNAL_DEPS = [
111
- 'obsidian',
112
- 'electron',
113
- '@codemirror/autocomplete',
114
- '@codemirror/collab',
115
- '@codemirror/commands',
116
- '@codemirror/language',
117
- '@codemirror/lint',
118
- '@codemirror/search',
119
- '@codemirror/state',
120
- '@codemirror/view',
121
- '@lezer/common',
122
- '@lezer/highlight',
123
- '@lezer/lr',
124
- ...builtins
125
- ];
126
-
127
- const BANNER = `/*
128
- THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
129
- if you want to view the source, please visit the github repository of this plugin
130
- */`;
131
-
132
- async function validateEnvironment(): Promise<void> {
133
- const srcMainPath = path.join(pluginDir, 'src/main.ts');
134
- if (!(await isValidPath(srcMainPath))) {
135
- throw new Error(
136
- 'Invalid path for src/main.ts. main.ts must be in the src directory'
137
- );
138
- }
139
- if (!(await isValidPath(manifestPath))) {
140
- throw new Error('Invalid path for manifest.json');
141
- }
142
- }
143
-
144
- async function getBuildPath(isProd: boolean): Promise<string> {
145
- // Check if we should use real vault (either -r flag or "real" argument)
146
- const useRealVault = process.argv.includes('-r') || process.argv.includes('real');
147
-
148
- // If production build without redirection, return plugin directory
149
- if (isProd && !useRealVault) {
150
- return pluginDir;
151
- }
152
-
153
- // Determine which path to use
154
- const envKey = useRealVault ? 'REAL_VAULT' : 'TEST_VAULT';
155
- const vaultPath = process.env[envKey]?.trim();
156
-
157
- // If empty or undefined, we're already in the plugin folder
158
- if (!vaultPath) {
159
- // Check if we're in Obsidian plugins folder
160
- const currentPath = process.cwd();
161
- const isInObsidianPlugins =
162
- currentPath.includes('.obsidian/plugins') ||
163
- currentPath.includes('.obsidian\\plugins');
164
-
165
- if (isInObsidianPlugins) {
166
- // In obsidian/plugins: allow in-place development
167
- console.log(`ℹ️ Building in Obsidian plugins folder (in-place development)`);
168
- return pluginDir;
169
- } else {
170
- // External development: prompt for missing vault path
171
- const newPath = await promptForVaultPath(envKey);
172
- await updateEnvFile(envKey, newPath);
173
- config();
174
- return getVaultPath(newPath);
175
- }
176
- }
177
-
178
- // If we reach here, use the vault path directly
179
- return getVaultPath(vaultPath);
180
- }
181
-
182
- async function createBuildContext(
183
- buildPath: string,
184
- isProd: boolean,
185
- entryPoints: string[]
186
- ): Promise<esbuild.BuildContext> {
187
- return await esbuild.context({
188
- banner: { js: BANNER },
189
- minify: isProd,
190
- entryPoints,
191
- bundle: true,
192
- external: EXTERNAL_DEPS,
193
- format: 'cjs',
194
- target: 'esNext',
195
- platform: 'node',
196
- logLevel: 'info',
197
- sourcemap: isProd ? false : 'inline',
198
- treeShaking: true,
199
- outdir: buildPath,
200
- outbase: path.join(pluginDir, 'src'),
201
- plugins: [
202
- {
203
- name: 'copy-to-plugins-folder',
204
- setup: (build): void => {
205
- build.onEnd(async () => {
206
- // if real or build
207
- if (isProd) {
208
- if (
209
- process.argv.includes('-r') ||
210
- process.argv.includes('real')
211
- ) {
212
- await copyFilesToTargetDir(buildPath);
213
- console.log(`Successfully installed in ${buildPath}`);
214
- } else {
215
- const folderToRemove = path.join(buildPath, '_.._');
216
- if (await isValidPath(folderToRemove)) {
217
- await rm(folderToRemove, { recursive: true });
218
- }
219
- console.log('Built done in initial folder');
220
- }
221
- }
222
- // if watch (dev)
223
- else {
224
- await copyFilesToTargetDir(buildPath);
225
- }
226
- });
227
- }
228
- }
229
- ]
230
- });
231
- }
232
-
233
- async function main(): Promise<void> {
234
- try {
235
- await validateEnvironment();
236
- const isProd = process.argv[2] === 'production';
237
- const buildPath = await getBuildPath(isProd);
238
- console.log(
239
- buildPath === pluginDir
240
- ? 'Building in initial folder'
241
- : `Building in ${buildPath}`
242
- );
243
- const srcStylesPath = path.join(pluginDir, 'src/styles.css');
244
- const rootStylesPath = path.join(pluginDir, 'styles.css');
245
- const stylePath = (await isValidPath(srcStylesPath))
246
- ? srcStylesPath
247
- : (await isValidPath(rootStylesPath))
248
- ? rootStylesPath
249
- : '';
250
- const mainTsPath = path.join(pluginDir, 'src/main.ts');
251
- const entryPoints = stylePath ? [mainTsPath, stylePath] : [mainTsPath];
252
- const context = await createBuildContext(buildPath, isProd, entryPoints);
253
-
254
- if (isProd) {
255
- await context.rebuild();
256
- rl.close();
257
- process.exit(0);
258
- } else {
259
- await context.watch();
260
- }
261
- } catch (error) {
262
- console.error('Build failed:', error);
263
- rl.close();
264
- process.exit(1);
265
- }
266
- }
267
-
268
- main().catch(console.error);
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env tsx
2
-
3
- import fs from 'fs';
4
- import path from 'path';
5
-
6
- const TEMPLATES_PKG = 'templates/package.json';
7
- const TEMPLATES_SASS = 'templates/package-sass.json';
8
-
9
- function resolvedVersion(dep: string): string | null {
10
- const pkgPath = path.join('node_modules', dep, 'package.json');
11
- if (!fs.existsSync(pkgPath)) return null;
12
- try {
13
- return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version as string;
14
- } catch {
15
- return null;
16
- }
17
- }
18
-
19
- function updateDeps(deps: Record<string, string>): { updated: string[] } {
20
- const updated: string[] = [];
21
-
22
- for (const dep of Object.keys(deps)) {
23
- const current = deps[dep];
24
- // Skip wildcards like "*"
25
- if (current === '*') continue;
26
-
27
- const resolved = resolvedVersion(dep);
28
- if (!resolved) continue;
29
-
30
- // Keep "latest" as-is, update pinned/range versions
31
- if (current !== 'latest') {
32
- const newVersion = `^${resolved}`;
33
- if (deps[dep] !== newVersion) {
34
- deps[dep] = newVersion;
35
- updated.push(`${dep}: ${current} → ${newVersion}`);
36
- }
37
- }
38
- }
39
-
40
- return { updated };
41
- }
42
-
43
- function syncFile(filePath: string): void {
44
- const pkg = JSON.parse(fs.readFileSync(filePath, 'utf8'));
45
- const { updated } = updateDeps(pkg.devDependencies ?? {});
46
-
47
- if (updated.length) {
48
- fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
49
- console.log(`\n✅ ${filePath}`);
50
- for (const u of updated) console.log(` ${u}`);
51
- } else {
52
- console.log(`\n✅ ${filePath} — already up to date`);
53
- }
54
- }
55
-
56
- console.log('🔄 Syncing template deps from node_modules...');
57
- syncFile(TEMPLATES_PKG);
58
- syncFile(TEMPLATES_SASS);
59
- console.log('\n✅ Done.');
@@ -1,82 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
-
5
- // Get current directory (equivalent to __dirname in CommonJS)
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
-
9
- /**
10
- * Scans the src directory for subdirectories with index.ts files
11
- * and generates the main index.ts with appropriate exports
12
- */
13
- function updateExports() {
14
- console.log('🔄 Updating exports...');
15
-
16
- const srcDir = path.join(__dirname, '../src');
17
- const mainIndexPath = path.join(srcDir, 'index.ts');
18
-
19
- try {
20
- // Check if src directory exists
21
- if (!fs.existsSync(srcDir)) {
22
- console.error('❌ src directory not found');
23
- return;
24
- }
25
-
26
- // Get all subdirectories in src/
27
- const items = fs.readdirSync(srcDir, { withFileTypes: true });
28
- const subdirs = items
29
- .filter(item => item.isDirectory())
30
- .map(item => item.name);
31
-
32
- // Find subdirectories that have an index.ts file
33
- const exportsToGenerate = [];
34
-
35
- for (const subdir of subdirs) {
36
- const subdirPath = path.join(srcDir, subdir);
37
- const indexPath = path.join(subdirPath, 'index.ts');
38
-
39
- if (fs.existsSync(indexPath)) {
40
- exportsToGenerate.push(subdir);
41
- console.log(`✅ Found exportable module: ${subdir}`);
42
- }
43
- }
44
-
45
- // Generate the export statements
46
- const exportStatements = exportsToGenerate
47
- .map(subdir => `export * from './${subdir}/index.js';`)
48
- .join('\n');
49
-
50
- // Add header comment
51
- const fileContent = `// Auto-generated exports - DO NOT EDIT MANUALLY
52
- // Run 'npm run update-exports' to regenerate this file
53
-
54
- ${exportStatements}
55
- `;
56
-
57
- // Write the main index.ts file
58
- fs.writeFileSync(mainIndexPath, fileContent, 'utf8');
59
-
60
- // Update package.json exports
61
- const packageJsonPath = path.join(__dirname, '../package.json');
62
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
63
-
64
- // Generate exports object
65
- const exportsObj = { ".": "./src/index.ts" };
66
- packageJson.exports = exportsObj;
67
-
68
- // Write updated package.json
69
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
70
-
71
- console.log(`✅ Updated ${mainIndexPath}`);
72
- console.log(`✅ Updated ${packageJsonPath} exports`);
73
- console.log(`📦 Generated ${exportsToGenerate.length} export(s): ${exportsToGenerate.join(', ')}`);
74
-
75
- } catch (error) {
76
- console.error('❌ Error updating exports:', error.message);
77
- process.exit(1);
78
- }
79
- }
80
-
81
- // Run the function
82
- updateExports();
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- // Auto-generated exports - DO NOT EDIT MANUALLY
2
- // Run 'npm run update-exports' to regenerate this file
3
-
4
- export * from './modals/index.js';
5
- export * from './tools/index.js';
6
- export * from './utils/index.js';
package/src/main.ts DELETED
@@ -1,117 +0,0 @@
1
- import type { App } from 'obsidian';
2
- import { Plugin, PluginSettingTab, Setting, Notice } from 'obsidian';
3
- import { showConfirmModal } from './modals/GenericConfirmModal.ts';
4
- // import { showTestMessage, getRandomEmoji } from "obsidian-plugin-config/tools";
5
-
6
- interface MyPluginSettings {
7
- mySetting: string;
8
- }
9
-
10
- const DEFAULT_SETTINGS: MyPluginSettings = {
11
- mySetting: 'default'
12
- };
13
-
14
- export default class ObsidianPluginConfigPlugin extends Plugin {
15
- settings: MyPluginSettings;
16
-
17
- async onload(): Promise<void> {
18
- console.debug('Loading obsidian-plugin-config plugin for testing NPM exports');
19
- await this.loadSettings();
20
-
21
- this.addCommand({
22
- id: 'show-confirmation-modal',
23
- name: 'Test Confirmation Modal (Local)',
24
- callback: () => this.showConfirmationModal()
25
- });
26
-
27
- this.addCommand({
28
- id: 'show-centralized-modal',
29
- name: 'Test Confirmation Modal (Centralized)',
30
- callback: () => this.showCentralizedModal()
31
- });
32
-
33
- this.addCommand({
34
- id: 'test-tools',
35
- name: 'Test Centralized Tools',
36
- callback: () => {
37
- // const message = showTestMessage();
38
- // const emoji = getRandomEmoji();
39
- new Notice(`🎯 Test centralized tools (commented for autonomous mode)`);
40
- }
41
- });
42
-
43
- this.addSettingTab(new PluginConfigSettingTab(this.app, this));
44
- }
45
-
46
- private showConfirmationModal(): void {
47
- showConfirmModal(this.app, {
48
- title: 'Confirmation requise',
49
- message:
50
- 'Êtes-vous sûr de vouloir effectuer cette action ? Cette action ne peut pas être annulée.',
51
- confirmText: 'Confirmer',
52
- cancelText: 'Annuler',
53
- onConfirm: () => {
54
- new Notice('Action confirmée !');
55
- },
56
- onCancel: () => {
57
- new Notice('Action annulée.');
58
- }
59
- });
60
- }
61
-
62
- private showCentralizedModal(): void {
63
- showConfirmModal(this.app, {
64
- title: 'Centralized Modal Test',
65
- message:
66
- 'This modal comes from the centralized configuration! Pretty cool, right?',
67
- confirmText: 'Awesome!',
68
- cancelText: 'Not bad',
69
- onConfirm: () => {
70
- new Notice('Centralized modal confirmed! 🎉');
71
- },
72
- onCancel: () => {
73
- new Notice('Centralized modal cancelled 😢');
74
- }
75
- });
76
- }
77
-
78
- async loadSettings(): Promise<void> {
79
- this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
80
- }
81
-
82
- async saveSettings(): Promise<void> {
83
- await this.saveData(this.settings);
84
- }
85
- }
86
-
87
- class PluginConfigSettingTab extends PluginSettingTab {
88
- plugin: ObsidianPluginConfigPlugin;
89
-
90
- constructor(app: App, plugin: ObsidianPluginConfigPlugin) {
91
- super(app, plugin);
92
- this.plugin = plugin;
93
- }
94
-
95
- display(): void {
96
- const { containerEl } = this;
97
- containerEl.empty();
98
-
99
- containerEl.createEl('h2', { text: 'Obsidian Plugin Config Settings' });
100
- containerEl.createEl('p', {
101
- text: 'This plugin is used for testing NPM exports and development of the obsidian-plugin-config system.'
102
- });
103
-
104
- new Setting(containerEl)
105
- .setName('Test Setting')
106
- .setDesc('A test setting for development purposes')
107
- .addText((text) =>
108
- text
109
- .setPlaceholder('Enter test value')
110
- .setValue(this.plugin.settings.mySetting)
111
- .onChange(async (value) => {
112
- this.plugin.settings.mySetting = value;
113
- await this.plugin.saveSettings();
114
- })
115
- );
116
- }
117
- }
@@ -1,79 +0,0 @@
1
- import type { App } from 'obsidian';
2
- import { Modal } from 'obsidian';
3
-
4
- export interface ConfirmModalOptions {
5
- title: string;
6
- message: string;
7
- confirmText?: string;
8
- cancelText?: string;
9
- onConfirm: () => void;
10
- onCancel?: () => void;
11
- }
12
-
13
- export class GenericConfirmModal extends Modal {
14
- private options: ConfirmModalOptions;
15
-
16
- constructor(app: App, options: ConfirmModalOptions) {
17
- super(app);
18
- this.options = options;
19
- }
20
-
21
- onOpen(): void {
22
- const { contentEl } = this;
23
- contentEl.empty();
24
-
25
- const title =
26
- this.options.title.charAt(0).toUpperCase() + this.options.title.slice(1);
27
- contentEl.createEl('h2', { text: title });
28
-
29
- // Message
30
- contentEl.createEl('p', { text: this.options.message });
31
-
32
- // Buttons container
33
- const buttonContainer = contentEl.createDiv('modal-button-container');
34
-
35
- // Cancel button
36
- const cancelBtn = buttonContainer.createEl('button', {
37
- text: this.options.cancelText || 'Cancel',
38
- cls: 'mod-cta'
39
- });
40
- cancelBtn.addEventListener('click', () => {
41
- this.options.onCancel?.();
42
- this.close();
43
- });
44
-
45
- // Confirm button
46
- const confirmBtn = buttonContainer.createEl('button', {
47
- text: this.options.confirmText || 'Confirm',
48
- cls: 'mod-cta mod-warning'
49
- });
50
- confirmBtn.addEventListener('click', () => {
51
- this.options.onConfirm();
52
- this.close();
53
- });
54
-
55
- // Focus on confirm button
56
- confirmBtn.focus();
57
- }
58
-
59
- onClose(): void {
60
- const { contentEl } = this;
61
- contentEl.empty();
62
- }
63
- }
64
-
65
- // Utility function for quick usage
66
- export function showConfirmModal(app: App, options: ConfirmModalOptions): void {
67
- new GenericConfirmModal(app, options).open();
68
- }
69
-
70
- export async function confirmation(app: App, message: string): Promise<boolean> {
71
- return new Promise((resolve) => {
72
- new GenericConfirmModal(app, {
73
- title: 'Confirm',
74
- message,
75
- onConfirm: () => resolve(true),
76
- onCancel: () => resolve(false)
77
- }).open();
78
- });
79
- }
@@ -1,7 +0,0 @@
1
- // Export all modals for easy importing
2
- export {
3
- GenericConfirmModal,
4
- showConfirmModal,
5
- confirmation
6
- } from './GenericConfirmModal.js';
7
- export type { ConfirmModalOptions } from './GenericConfirmModal.js';
@@ -1,9 +0,0 @@
1
- // Simple tools for testing
2
- export function showTestMessage(): string {
3
- return '✅ CENTRALIZED TOOLS WORK! This comes from obsidian-plugin-config!';
4
- }
5
-
6
- export function getRandomEmoji(): string {
7
- const emojis = ['🚀', '🎉', '✨', '🔥', '💯', '⚡', '🎯', '🌟'];
8
- return emojis[Math.floor(Math.random() * emojis.length)];
9
- }