Compare commits

...

22 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
MarsBatya
4dfd698fee bump version to 1.1.1 2025-04-08 23:10:20 +05:00
MarsBatya
8c11a98791 Add release workflow 2025-04-08 23:07:30 +05:00
MarsBatya
e1f7322e14 stop triggering when there is a space after triggerChar 2025-04-08 23:06:23 +05:00
MarsBatya
22f0394b64 remove headings created w/ createEl and rename Advanced to General 2025-03-24 17:02:22 +05:00
MarsBatya
72aedd0d49 update texts in ui w/ sentence case 2025-03-24 16:50:29 +05:00
MarsBatya
614d2eea60 remove Obsidian from the readme header 2025-03-24 16:46:04 +05:00
MarsBatya
e7eee09cfe update license 2025-03-24 16:43:00 +05:00
16 changed files with 10844 additions and 5060 deletions

35
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css

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/*

22
LICENSE
View file

@ -1,5 +1,21 @@
Copyright (C) 2020-2025 by Dynalist Inc.
MIT License
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
Copyright (c) 2025 MarsBatya
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,6 +1,9 @@
# Obsidian "TG Emoji Search" Plugin
# 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

202
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,30 +67,25 @@ 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', () => {
const ribbonIconEl = this.addRibbonIcon('smile', 'Emoji suggester', () => {
new Notice(`Type "${this.settings.triggerChar}" followed by a keyword to suggest emojis`);
});
ribbonIconEl.addClass('emoji-suggester-ribbon-class');
@ -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а-яА-Я ]*)$`);
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) {
@ -269,22 +333,67 @@ class EmojiSuggesterSettingTab extends PluginSettingTab {
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Advanced' });
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('Trigger Character')
.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')
.addText(text => text
.setPlaceholder(':')
@ -306,7 +415,7 @@ class EmojiSuggesterSettingTab extends PluginSettingTab {
}));
// Add section for custom emoji mappings
containerEl.createEl('h2', { text: 'Custom Emoji Mappings' });
new Setting(containerEl).setName('Custom emoji mappings').setHeading();
// Display existing custom mappings
const customMappingsContainer = containerEl.createDiv('custom-emoji-mappings-container');
@ -350,7 +459,7 @@ class EmojiSuggesterSettingTab extends PluginSettingTab {
// Reset Custom Mappings button
new Setting(containerEl)
.setName('Reset Custom Emoji Mappings')
.setName('Reset custom emoji mappings')
.setDesc('Remove all your custom emoji mappings')
.addButton(button => button
.setButtonText('Reset')
@ -363,7 +472,7 @@ class EmojiSuggesterSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Reset Emoji Popularity')
.setName('Reset emoji popularity')
.setDesc('Reset your emoji usage statistics')
.addButton(button => button
.setButtonText('Reset')
@ -383,7 +492,7 @@ class EmojiSuggesterSettingTab extends PluginSettingTab {
// Display top 10 most used emojis
const topEmojisEl = containerEl.createDiv();
topEmojisEl.addClass('emoji-popularity-stats');
topEmojisEl.createEl('h2', { text: 'Your Most Used Emojis' });
topEmojisEl.createEl('h2', { text: 'Your most used emojis' });
const popularityEntries = Object.entries(this.plugin.settings.emojiPopularity);
popularityEntries.sort((a, b) => b[1] - a[1]);
@ -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.0",
"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;
}