Compare commits

..

15 commits

Author SHA1 Message Date
MarsBatya
f2162038b6 bump version to 1.2.1 2026-06-01 22:50:29 +05:00
MarsBatya
28822f1ed2 update russian and spanish words 2026-06-01 22:46:42 +05:00
MarsBatya
73052d87bf bump version to 1.2.0 2026-06-01 22:10:34 +05:00
MarsBatya
e66b69886b add a toggle for lazy loading (default is True) 2026-06-01 16:26:54 +05:00
MarsBatya
9198cc2f14 reduce startup time by introducing the lazy loading 2026-06-01 16:02:22 +05:00
MarsBatya
de6407f615 add scripts folder to gitignore 2025-09-29 15:58:17 +05:00
MarsBatya
17b8164a84 bump version to 1.1.4 2025-09-29 15:46:43 +05:00
MarsBatya
cf2f7e112b update russian keywords 2025-09-29 15:44:39 +05:00
MarsBatya
fa83477852 Merge branch 'master' of https://github.com/MarsBatya/tg-emoji-search 2025-09-29 00:40:36 +05:00
MarsBatya
6ec21a16bd bump version 2025-09-29 00:40:32 +05:00
MarsBatya
9f2a661b56 introduce spanish language and make language choice more flexible 2025-09-29 00:40:04 +05:00
MarsBatya
c56e4e943d remove esbuild-plugin-copy from devdependencies 2025-09-29 00:35:03 +05:00
MarsBatya
1fb74fcc72 add rust temp files to gitignore 2025-09-29 00:34:05 +05:00
tg:@mars_hall
b04b97830b
Update README.md
add somewhat of a logo and make demo gif smaller
2025-06-17 11:14:15 +05:00
MarsBatya
6572cbbfc3 bump version to 1.1.2 2025-04-08 23:22:49 +05:00
14 changed files with 10782 additions and 5049 deletions

6
.gitignore vendored
View file

@ -20,3 +20,9 @@ data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# Exclude rust temp files
target/*
# Exclude script helpers
scripts/*

View file

@ -1,6 +1,9 @@
# TG Emoji Search Plugin
![demo](https://github.com/user-attachments/assets/22517d21-6493-430e-bd03-3b2cc6f41f1e)
<img src="https://github.com/user-attachments/assets/e059f517-78b4-4734-b34e-3a5b102acdce" alt="some logo" width="200"/>
## Simple demo
<img src="https://github.com/user-attachments/assets/22517d21-6493-430e-bd03-3b2cc6f41f1e" alt="demo" width="600"/>
This plugin provides a fast and intuitive way to insert emojis into your Obsidian notes. By typing a trigger character (":" by default) followed by a keyword, you get an instant suggestion list of matching emojis, complete with optional keyword hints. The plugin also includes a command to insert a random emoji, plus it tracks your most used emojis.
@ -14,7 +17,8 @@ This plugin provides a fast and intuitive way to insert emojis into your Obsidia
- Uses data inspired by Telegrams emoji translations.
- English keywords: ~4k, emojis: ~2k
- Russian keywords: ~5k, emojis: ~2k
- Total keywords: ~9k, emojis: ~2k
- Spanish keywords: ~5k, emojis: ~2k
- Total keywords: ~13k, emojis: ~2k
3. **Fast Performance with Rust + WebAssembly**
- The core emoji search is implemented in Rust, compiled to WebAssembly for speed.
@ -58,6 +62,7 @@ This plugin provides a fast and intuitive way to insert emojis into your Obsidia
- Emoji keywords data is adapted from Telegrams translations:
- [Russian](https://translations.telegram.org/ru/emoji)
- [English](https://translations.telegram.org/en/emoji)
- [Spanish](https://translations.telegram.org/es/emoji)
## Contributing

File diff suppressed because it is too large Load diff

5068
emoji_data_spanish.json Normal file

File diff suppressed because it is too large Load diff

188
main.ts
View file

@ -15,6 +15,7 @@ import {
import init, { EmojiSearch } from './pkg/emoji_search_fixed.js';
import emojiDataEnglish from "./emoji_data_english.json";
import emojiDataRussian from "./emoji_data_russian.json";
import emojiDataSpanish from "./emoji_data_spanish.json";
import wasmDataUrl from "./pkg/emoji_search_bg.wasm";
// --- Type Definitions ---
@ -28,27 +29,34 @@ interface InitOutput {
}
interface EmojiSuggesterPluginSettings {
defaultLanguage: string;
chosenLanguages: string[];
triggerChar: string;
showKeywords: boolean;
emojiPopularity: Record<string, number>;
customEmojiMappings: Record<string, string>;
lazyLoadWasm: boolean;
}
const DEFAULT_SETTINGS: EmojiSuggesterPluginSettings = {
defaultLanguage: 'english',
chosenLanguages: ['english'],
triggerChar: ':',
showKeywords: true,
emojiPopularity: {},
customEmojiMappings: {},
lazyLoadWasm: true,
};
const AVAILABLE_LANGUAGES = {
english: 'English',
russian: 'Russian',
spanish: 'Spanish'
};
const RANDOM_EMOJIS = ['😀', '😂', '🥰', '😎', '🤔', '👍', '🎉', '✨', '🔥', '❤️'];
async function loadEmojiData() {
// The JSON is already inlined as objects
return { english: emojiDataEnglish, russian: emojiDataRussian };
return { english: emojiDataEnglish, russian: emojiDataRussian, spanish: emojiDataSpanish };
}
// --- Main Plugin Class ---
@ -59,27 +67,22 @@ export default class EmojiSuggesterPlugin extends Plugin {
async onload() {
await this.loadSettings();
try {
// Initialize WASM module using a binary fetch approach
await this.loadWasmModule();
// Create instance of EmojiSearch and initialize with emoji data
this.emojiSearch = new EmojiSearch();
const emojiData = await loadEmojiData();
this.emojiSearch.initialize(JSON.stringify(emojiData));
console.log('WASM initialized successfully with emoji data');
// Register editor suggestions using our custom suggester
this.registerEditorSuggest(new EmojiSuggester(this.app, this, this.emojiSearch));
// Uncomment to have a success notice:
// new Notice('Emoji Suggester plugin loaded successfully');
} catch (error) {
console.error('Failed to initialize WASM:', error);
new Notice('Failed to initialize Emoji suggester plugin');
if (!this.settings.lazyLoadWasm) {
try {
await this.loadWasmModule();
this.emojiSearch = new EmojiSearch();
await this.reinitializeEmojiSearch(this.emojiSearch);
console.log('Emoji suggester plugin initialized successfully');
} catch (error) {
console.error('Failed to initialize WASM:', error);
new Notice('Failed to initialize Emoji suggester plugin');
}
}
// Add a settings tab for the plugin
this.addSettingTab(new EmojiSuggesterSettingTab(this.app, this));
this.registerEditorSuggest(new EmojiSuggester(this.app, this, this.emojiSearch));
// Create a ribbon icon with a tooltip
const ribbonIconEl = this.addRibbonIcon('smile', 'Emoji suggester', () => {
@ -98,6 +101,17 @@ export default class EmojiSuggesterPlugin extends Plugin {
});
}
async reinitializeEmojiSearch(emojiSearch: EmojiSearch): Promise<void> {
const allEmojiData = await loadEmojiData();
const filteredData: Record<string, any> = {};
for (const lang of this.settings.chosenLanguages) {
if (allEmojiData[lang as keyof typeof allEmojiData]) {
filteredData[lang] = allEmojiData[lang as keyof typeof allEmojiData];
}
}
emojiSearch.initialize(JSON.stringify(filteredData));
}
async loadWasmModule(): Promise<InitOutput> {
try {
const wasmModule = await import('./pkg/emoji_search_fixed.js');
@ -123,31 +137,61 @@ export default class EmojiSuggesterPlugin extends Plugin {
}
async loadSettings() {
this.settings = { ...DEFAULT_SETTINGS, ...(await this.loadData() || {}) };
const loadedSettings = await this.loadData() || {};
// Migration: convert old defaultLanguage to chosenLanguages
if (loadedSettings.defaultLanguage && !loadedSettings.chosenLanguages) {
loadedSettings.chosenLanguages = [loadedSettings.defaultLanguage];
delete loadedSettings.defaultLanguage;
}
this.settings = { ...DEFAULT_SETTINGS, ...loadedSettings };
}
async saveSettings() {
await this.saveData(this.settings);
// Reinitialize emoji search when settings change
if (this.emojiSearch) {
await this.reinitializeEmojiSearch(this.emojiSearch);
}
}
}
class EmojiSuggester extends EditorSuggest<string> {
private emojiSearch: EmojiSearch;
private emojiSearch: EmojiSearch | null = null;
private settings: EmojiSuggesterPluginSettings;
private plugin: EmojiSuggesterPlugin;
private wasmReady = false;
private initPromise: Promise<void> | null = null;
constructor(app: App, plugin: EmojiSuggesterPlugin, emojiSearch: EmojiSearch) {
constructor(app: App, plugin: EmojiSuggesterPlugin, emojiSearch: EmojiSearch | null = null) {
super(app);
this.plugin = plugin;
this.emojiSearch = emojiSearch;
this.settings = plugin.settings;
this.emojiSearch = emojiSearch;
this.wasmReady = this.emojiSearch != null;
}
private async ensureWasmReady(): Promise<void> {
if (this.wasmReady) return;
if (!this.plugin.settings.lazyLoadWasm) return;
if (this.initPromise) return this.initPromise;
this.initPromise = (async () => {
await this.plugin.loadWasmModule();
this.emojiSearch = new EmojiSearch();
await this.plugin.reinitializeEmojiSearch(this.emojiSearch);
console.log('WASM initialized successfully with emoji data');
this.wasmReady = true;
})();
return this.initPromise;
}
onTrigger(cursor: EditorPosition, editor: Editor): EditorSuggestTriggerInfo | null {
const line = editor.getLine(cursor.line);
const subString = line.substring(0, cursor.ch);
const escapedTrigger = this.settings.triggerChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const triggerRegex = new RegExp(`${escapedTrigger}([a-zA-Zа-яА-Я][a-zA-Zа-яА-Я ]*)$`);
const triggerRegex = new RegExp(`${escapedTrigger}([a-zA-Zа-яА-Яñáéíóúü][a-zA-Zа-яА-Яñáéíóúü ]*)$`);
const match = subString.match(triggerRegex);
if (!match) return null;
return {
@ -156,15 +200,34 @@ class EmojiSuggester extends EditorSuggest<string> {
query: match[1]?.trim() || ''
};
}
getSuggestions(context: EditorSuggestContext): string[] {
if (!context.query || !this.emojiSearch) return [];
const language = /[а-яА-Я]/.test(context.query)
? 'russian'
: this.settings.defaultLanguage;
getSuggestions(context: EditorSuggestContext): string[] {
if (!this.wasmReady || !this.emojiSearch) {
this.ensureWasmReady();
return [];
}
if (!context.query || !this.emojiSearch) return [];
try {
// Get results from the WASM module
const results: [string, string[]][] = JSON.parse(this.emojiSearch.search(context.query, language));
// Search across all chosen languages
// Detect language based on query characters and filter chosen languages
const detectedLanguages = this.settings.chosenLanguages.filter(lang => {
if (/[а-яА-Я]/.test(context.query)) {
return lang === 'russian';
} else if (/[ñáéíóúü¿¡]/.test(context.query)) {
return lang === 'spanish';
} else {
// Default to non-Cyrillic languages for Latin script
return lang !== 'russian';
}
});
// If no languages match the detected script, fall back to all chosen languages
const languagesToSearch = detectedLanguages.length > 0 ? detectedLanguages : this.settings.chosenLanguages;
// Search only the filtered languages
const results: [string, string[]][] = JSON.parse(
this.emojiSearch.search_multiple(context.query, JSON.stringify(languagesToSearch))
);
const emojiMap = new Map<string, string>();
// Add emojis from WASM search results
@ -205,6 +268,7 @@ class EmojiSuggester extends EditorSuggest<string> {
return [];
}
}
renderSuggestion(value: string, el: HTMLElement): void {
el.empty();
if (this.settings.showKeywords) {
@ -272,17 +336,62 @@ class EmojiSuggesterSettingTab extends PluginSettingTab {
new Setting(containerEl).setName('General').setHeading();
new Setting(containerEl)
.setName('Default language')
.setDesc('Choose the default language for emoji search')
.addDropdown(dropdown => dropdown
.addOption('english', 'English')
.addOption('russian', 'Russian')
.setValue(this.plugin.settings.defaultLanguage)
.setName('Lazy load emoji engine')
.setDesc('Load the emoji search engine on first use instead of at startup. Reduces Obsidian startup time but adds a small delay the first time you trigger suggestions.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.lazyLoadWasm)
.onChange(async (value) => {
this.plugin.settings.defaultLanguage = value;
this.plugin.settings.lazyLoadWasm = value;
await this.plugin.saveSettings();
new Notice('Restart Obsidian for this change to take effect');
}));
new Setting(containerEl)
.setName('Chosen languages')
.setDesc('Select which languages to enable for emoji search')
.setClass('emoji-language-setting');
// Create checkboxes for each available language
const languageContainer = containerEl.createDiv('language-checkbox-container');
for (const [langCode, langName] of Object.entries(AVAILABLE_LANGUAGES)) {
const checkboxContainer = languageContainer.createDiv('language-checkbox-item');
const checkbox = checkboxContainer.createEl('input');
checkbox.type = 'checkbox';
checkbox.id = `lang-${langCode}`;
checkbox.checked = this.plugin.settings.chosenLanguages.includes(langCode);
const label = checkboxContainer.createEl('label');
label.setAttribute('for', `lang-${langCode}`);
label.textContent = langName;
checkbox.addEventListener('change', async (e) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
if (!this.plugin.settings.chosenLanguages.includes(langCode)) {
this.plugin.settings.chosenLanguages.push(langCode);
}
} else {
const index = this.plugin.settings.chosenLanguages.indexOf(langCode);
if (index > -1) {
this.plugin.settings.chosenLanguages.splice(index, 1);
}
}
// Ensure at least one language is selected
if (this.plugin.settings.chosenLanguages.length === 0) {
this.plugin.settings.chosenLanguages.push('english');
// Update the english checkbox
const englishCheckbox = containerEl.querySelector('#lang-english') as HTMLInputElement;
if (englishCheckbox) englishCheckbox.checked = true;
new Notice('At least one language must be selected. English has been re-enabled.');
}
await this.plugin.saveSettings();
});
}
new Setting(containerEl)
.setName('Trigger character')
.setDesc('Character that triggers the emoji suggestions')
@ -408,10 +517,7 @@ class EmojiSuggesterSettingTab extends PluginSettingTab {
countSpan.addClass('emoji-top-count');
countSpan.textContent = `Used ${count} time${count !== 1 ? 's' : ''}`;
}
}
}
private renderCustomMappings(containerEl: HTMLElement): void {
containerEl.empty();

View file

@ -1,7 +1,7 @@
{
"id": "tg-emoji-search",
"name": "TG Emoji Search",
"version": "1.1.1",
"version": "1.2.1",
"minAppVersion": "0.15.0",
"description": "Lets you type out emojis like in Telegram.",
"author": "MarsBatya",

10
pkg/emoji_search.d.ts vendored
View file

@ -4,7 +4,12 @@ export class EmojiSearch {
free(): void;
constructor();
initialize(emoji_data_json: string): void;
update_language(language_code: string, language_data_json: string): void;
remove_language(language_code: string): void;
search(query: string, language: string): string;
search_multiple(query: string, languages_json: string): string;
get_languages(): string;
get_stats(): string;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
@ -14,7 +19,12 @@ export interface InitOutput {
readonly __wbg_emojisearch_free: (a: number, b: number) => void;
readonly emojisearch_new: () => number;
readonly emojisearch_initialize: (a: number, b: number, c: number) => [number, number];
readonly emojisearch_update_language: (a: number, b: number, c: number, d: number, e: number) => [number, number];
readonly emojisearch_remove_language: (a: number, b: number, c: number) => void;
readonly emojisearch_search: (a: number, b: number, c: number, d: number, e: number) => [number, number];
readonly emojisearch_search_multiple: (a: number, b: number, c: number, d: number, e: number) => [number, number];
readonly emojisearch_get_languages: (a: number) => [number, number];
readonly emojisearch_get_stats: (a: number) => [number, number];
readonly __wbindgen_export_0: WebAssembly.Table;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;

View file

@ -114,6 +114,28 @@ export class EmojiSearch {
throw takeFromExternrefTable0(ret[0]);
}
}
/**
* @param {string} language_code
* @param {string} language_data_json
*/
update_language(language_code, language_data_json) {
const ptr0 = passStringToWasm0(language_code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(language_data_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.emojisearch_update_language(this.__wbg_ptr, ptr0, len0, ptr1, len1);
if (ret[1]) {
throw takeFromExternrefTable0(ret[0]);
}
}
/**
* @param {string} language_code
*/
remove_language(language_code) {
const ptr0 = passStringToWasm0(language_code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
wasm.emojisearch_remove_language(this.__wbg_ptr, ptr0, len0);
}
/**
* @param {string} query
* @param {string} language
@ -135,6 +157,57 @@ export class EmojiSearch {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} query
* @param {string} languages_json
* @returns {string}
*/
search_multiple(query, languages_json) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(query, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(languages_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.emojisearch_search_multiple(this.__wbg_ptr, ptr0, len0, ptr1, len1);
deferred3_0 = ret[0];
deferred3_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @returns {string}
*/
get_languages() {
let deferred1_0;
let deferred1_1;
try {
const ret = wasm.emojisearch_get_languages(this.__wbg_ptr);
deferred1_0 = ret[0];
deferred1_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
}
}
/**
* @returns {string}
*/
get_stats() {
let deferred1_0;
let deferred1_1;
try {
const ret = wasm.emojisearch_get_stats(this.__wbg_ptr);
deferred1_0 = ret[0];
deferred1_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
}
}
}
async function __wbg_load(module, imports) {

Binary file not shown.

View file

@ -4,7 +4,12 @@ export const memory: WebAssembly.Memory;
export const __wbg_emojisearch_free: (a: number, b: number) => void;
export const emojisearch_new: () => number;
export const emojisearch_initialize: (a: number, b: number, c: number) => [number, number];
export const emojisearch_update_language: (a: number, b: number, c: number, d: number, e: number) => [number, number];
export const emojisearch_remove_language: (a: number, b: number, c: number) => void;
export const emojisearch_search: (a: number, b: number, c: number, d: number, e: number) => [number, number];
export const emojisearch_search_multiple: (a: number, b: number, c: number, d: number, e: number) => [number, number];
export const emojisearch_get_languages: (a: number) => [number, number];
export const emojisearch_get_stats: (a: number) => [number, number];
export const __wbindgen_export_0: WebAssembly.Table;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;

View file

@ -114,6 +114,28 @@ export class EmojiSearch {
throw takeFromExternrefTable0(ret[0]);
}
}
/**
* @param {string} language_code
* @param {string} language_data_json
*/
update_language(language_code, language_data_json) {
const ptr0 = passStringToWasm0(language_code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(language_data_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.emojisearch_update_language(this.__wbg_ptr, ptr0, len0, ptr1, len1);
if (ret[1]) {
throw takeFromExternrefTable0(ret[0]);
}
}
/**
* @param {string} language_code
*/
remove_language(language_code) {
const ptr0 = passStringToWasm0(language_code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
wasm.emojisearch_remove_language(this.__wbg_ptr, ptr0, len0);
}
/**
* @param {string} query
* @param {string} language
@ -135,6 +157,57 @@ export class EmojiSearch {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} query
* @param {string} languages_json
* @returns {string}
*/
search_multiple(query, languages_json) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(query, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(languages_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.emojisearch_search_multiple(this.__wbg_ptr, ptr0, len0, ptr1, len1);
deferred3_0 = ret[0];
deferred3_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @returns {string}
*/
get_languages() {
let deferred1_0;
let deferred1_1;
try {
const ret = wasm.emojisearch_get_languages(this.__wbg_ptr);
deferred1_0 = ret[0];
deferred1_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
}
}
/**
* @returns {string}
*/
get_stats() {
let deferred1_0;
let deferred1_1;
try {
const ret = wasm.emojisearch_get_stats(this.__wbg_ptr);
deferred1_0 = ret[0];
deferred1_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
}
}
}
async function __wbg_load(module, imports) {
@ -240,6 +313,9 @@ async function __wbg_init(module_or_path) {
}
if (typeof module_or_path === 'undefined') {
// instead of this:
// module_or_path = new URL('emoji_search_bg.wasm', import.meta.url);
// we use this:
module_or_path = new URL('emoji_search_bg.wasm');
}
const imports = __wbg_get_imports();

View file

@ -11,8 +11,5 @@
"types": "emoji_search.d.ts",
"sideEffects": [
"./snippets/*"
],
"devDependencies": {
"esbuild-plugin-copy": "^2.1.1"
}
}
]
}

View file

@ -1,12 +1,11 @@
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
// Main structure to hold emoji data
// Main structure to hold emoji data for multiple languages
#[wasm_bindgen]
pub struct EmojiSearch {
english_keywords: HashMap<String, Vec<String>>,
russian_keywords: HashMap<String, Vec<String>>,
// Maps language code to keyword->emojis mapping
language_keywords: HashMap<String, HashMap<String, Vec<String>>>,
}
#[wasm_bindgen]
@ -15,44 +14,36 @@ impl EmojiSearch {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
english_keywords: HashMap::new(),
russian_keywords: HashMap::new(),
language_keywords: HashMap::new(),
}
}
// Initialize with emoji data
// Initialize with emoji data - can be called multiple times to reinitialize
#[wasm_bindgen]
pub fn initialize(&mut self, emoji_data_json: &str) -> Result<(), JsValue> {
// Clear existing data for reinitialization
self.language_keywords.clear();
let emoji_data: serde_json::Value = serde_json::from_str(emoji_data_json)
.map_err(|e| JsValue::from_str(&format!("Failed to parse JSON: {}", e)))?;
// Process the data
// Process the data - each top-level key is a language code
if let Some(obj) = emoji_data.as_object() {
for (key, value) in obj {
if key == "english" {
if let Some(english_map) = value.as_object() {
for (keyword, emojis) in english_map {
if let Some(emoji_str) = emojis.as_str() {
let emoji_vec: Vec<String> = emoji_str
.split_whitespace()
.map(|s| s.to_string())
.collect();
self.english_keywords.insert(keyword.to_lowercase(), emoji_vec);
}
}
}
} else if key == "russian" {
if let Some(russian_map) = value.as_object() {
for (keyword, emojis) in russian_map {
if let Some(emoji_str) = emojis.as_str() {
let emoji_vec: Vec<String> = emoji_str
.split_whitespace()
.map(|s| s.to_string())
.collect();
self.russian_keywords.insert(keyword.to_lowercase(), emoji_vec);
}
for (language_code, language_data) in obj {
if let Some(language_map) = language_data.as_object() {
let mut keyword_map = HashMap::new();
for (keyword, emojis) in language_map {
if let Some(emoji_str) = emojis.as_str() {
let emoji_vec: Vec<String> = emoji_str
.split_whitespace()
.map(|s| s.to_string())
.collect();
keyword_map.insert(keyword.to_lowercase(), emoji_vec);
}
}
self.language_keywords.insert(language_code.to_lowercase(), keyword_map);
}
}
}
@ -60,14 +51,46 @@ impl EmojiSearch {
Ok(())
}
// Search for emojis
// Add or update a specific language's keywords (useful for runtime updates)
#[wasm_bindgen]
pub fn update_language(&mut self, language_code: &str, language_data_json: &str) -> Result<(), JsValue> {
let language_data: serde_json::Value = serde_json::from_str(language_data_json)
.map_err(|e| JsValue::from_str(&format!("Failed to parse JSON: {}", e)))?;
if let Some(language_map) = language_data.as_object() {
let mut keyword_map = HashMap::new();
for (keyword, emojis) in language_map {
if let Some(emoji_str) = emojis.as_str() {
let emoji_vec: Vec<String> = emoji_str
.split_whitespace()
.map(|s| s.to_string())
.collect();
keyword_map.insert(keyword.to_lowercase(), emoji_vec);
}
}
self.language_keywords.insert(language_code.to_lowercase(), keyword_map);
}
Ok(())
}
// Remove a language entirely
#[wasm_bindgen]
pub fn remove_language(&mut self, language_code: &str) {
self.language_keywords.remove(&language_code.to_lowercase());
}
// Search for emojis in a specific language
#[wasm_bindgen]
pub fn search(&self, query: &str, language: &str) -> String {
let query = query.to_lowercase();
let map = if language == "russian" {
&self.russian_keywords
} else {
&self.english_keywords
let language = language.to_lowercase();
let map = match self.language_keywords.get(&language) {
Some(map) => map,
None => return "[]".to_string(), // Language not found
};
let mut results: Vec<(String, Vec<String>)> = Vec::new();
@ -91,4 +114,74 @@ impl EmojiSearch {
"[]".to_string()
}
}
}
// Search across multiple languages (useful for multilingual users)
#[wasm_bindgen]
pub fn search_multiple(&self, query: &str, languages_json: &str) -> String {
let query = query.to_lowercase();
// Parse the languages array
let languages: Result<Vec<String>, _> = serde_json::from_str(languages_json);
let languages = match languages {
Ok(langs) => langs,
Err(_) => return "[]".to_string(),
};
let mut all_results: Vec<(String, Vec<String>)> = Vec::new();
let mut seen_keywords = std::collections::HashSet::new();
for language in languages {
let language = language.to_lowercase();
if let Some(map) = self.language_keywords.get(&language) {
// First look for exact matches
if let Some(emojis) = map.get(&query) {
if !seen_keywords.contains(&query) {
all_results.push((query.clone(), emojis.clone()));
seen_keywords.insert(query.clone());
}
}
// Then look for prefix matches
for (keyword, emojis) in map {
if keyword.starts_with(&query) && keyword != &query && !seen_keywords.contains(keyword) {
all_results.push((keyword.clone(), emojis.clone()));
seen_keywords.insert(keyword.clone());
}
}
}
}
// Convert results to JSON
if let Ok(json) = serde_json::to_string(&all_results) {
json
} else {
"[]".to_string()
}
}
// Get list of available languages
#[wasm_bindgen]
pub fn get_languages(&self) -> String {
let languages: Vec<&String> = self.language_keywords.keys().collect();
if let Ok(json) = serde_json::to_string(&languages) {
json
} else {
"[]".to_string()
}
}
// Get statistics about loaded data
#[wasm_bindgen]
pub fn get_stats(&self) -> String {
let mut stats = HashMap::new();
for (lang, keywords) in &self.language_keywords {
stats.insert(lang, keywords.len());
}
if let Ok(json) = serde_json::to_string(&stats) {
json
} else {
"{}".to_string()
}
}
}

View file

@ -113,4 +113,46 @@
margin-right: var(--emoji-margin-right);
min-width: 2.5rem;
text-align: center;
}
.emoji-language-setting {
margin-bottom: 0.5rem;
}
.language-checkbox-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
padding: 0.75rem;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background: var(--background-secondary);
}
.language-checkbox-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
}
.language-checkbox-item input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
.language-checkbox-item label {
cursor: pointer;
user-select: none;
font-size: 0.9rem;
color: var(--text-normal);
margin: 0;
}
.language-checkbox-item:hover {
background: var(--background-modifier-hover);
border-radius: 4px;
padding: 0.25rem 0.5rem;
margin: 0 -0.5rem;
}