mirror of
https://github.com/marsbatya/tg-emoji-search.git
synced 2026-07-22 12:20:25 +00:00
Compare commits
22 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2162038b6 | ||
|
|
28822f1ed2 | ||
|
|
73052d87bf | ||
|
|
e66b69886b | ||
|
|
9198cc2f14 | ||
|
|
de6407f615 | ||
|
|
17b8164a84 | ||
|
|
cf2f7e112b | ||
|
|
fa83477852 | ||
|
|
6ec21a16bd | ||
|
|
9f2a661b56 | ||
|
|
c56e4e943d | ||
|
|
1fb74fcc72 | ||
|
|
b04b97830b | ||
|
|
6572cbbfc3 | ||
|
|
4dfd698fee | ||
|
|
8c11a98791 | ||
|
|
e1f7322e14 | ||
|
|
22f0394b64 | ||
|
|
72aedd0d49 | ||
|
|
614d2eea60 | ||
|
|
e7eee09cfe |
16 changed files with 10844 additions and 5060 deletions
35
.github/workflows/release.yml
vendored
Normal file
35
.github/workflows/release.yml
vendored
Normal 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
6
.gitignore
vendored
|
|
@ -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
22
LICENSE
|
|
@ -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.
|
||||
11
README.md
11
README.md
|
|
@ -1,6 +1,9 @@
|
|||
# Obsidian "TG Emoji Search" Plugin
|
||||
# TG Emoji Search Plugin
|
||||
|
||||

|
||||
<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 Telegram’s 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 Telegram’s translations:
|
||||
- [Russian](https://translations.telegram.org/ru/emoji)
|
||||
- [English](https://translations.telegram.org/en/emoji)
|
||||
- [Spanish](https://translations.telegram.org/es/emoji)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
10178
emoji_data_russian.json
10178
emoji_data_russian.json
File diff suppressed because it is too large
Load diff
5068
emoji_data_spanish.json
Normal file
5068
emoji_data_spanish.json
Normal file
File diff suppressed because it is too large
Load diff
202
main.ts
202
main.ts
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
10
pkg/emoji_search.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
5
pkg/emoji_search_bg.wasm.d.ts
vendored
5
pkg/emoji_search_bg.wasm.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -11,8 +11,5 @@
|
|||
"types": "emoji_search.d.ts",
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"esbuild-plugin-copy": "^2.1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
167
src/lib.rs
167
src/lib.rs
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
42
styles.css
42
styles.css
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue