Fluent Emoji 3D: Unicode-Emojis durch Microsoft Fluent Emoji ersetzen
- emoji.js: Emoji-Replacement-Engine mit umfassendem Unicode 15.0 Regex - String-basiert (replaceEmojisWithFluent) in renderMarkdown() - DOM-basiert (parseFluentEmojis) als Post-Processing via TreeWalker - CDN: @lobehub/fluent-emoji-3d@1.1.0 via jsdelivr (WebP/3D) - Intelligenter Fallback: fe0f-Varianten, dann Original-Unicode - Fail-Cache verhindert wiederholte Requests für fehlende Emojis - User-Nachrichten werden jetzt als Markdown gerendert (renderMarkdown) - [Datei: ...] Refs: Platzhalter-Mechanismus vor Markdown-Parsing - Reply-Quotes bleiben durch Wrapper-Div erhalten - parseFluentEmojis an allen Render-Stellen integriert: addAssistantBubble, endStreamBubble, _renderUserTextContent, _renderSystemPromptBlocks, history._renderMessagesIntoFragment, history.loadLatestArchive - CSS: .fluent-emoji Klasse (1.2em, inline, scale 1.25) - CSS: white-space:pre-wrap nur noch für .msg:not(.markdown)
This commit is contained in:
1 parent
e2c887aee6
commit
5d132585d5
6 files changed
+156
-33
No files matched your search
@@ -0,0 +1,100 @@
|
||||
// ── Fluent Emoji Replacement ──
|
||||
// Replaces Unicode emojis in rendered Markdown HTML with Fluent Emoji 3D images.
|
||||
// Uses lobehub's CDN-hosted Microsoft Fluent Emoji 3D set (WebP format).
|
||||
|
||||
var FLUENT_EMOJI_CDN = 'https://cdn.jsdelivr.net/npm/@lobehub/fluent-emoji-3d@1.1.0/assets/';
|
||||
var _emojiFailCache = {}; // Track permanently failed emoji codepoints
|
||||
|
||||
// Comprehensive emoji regex (from AdvenaHQ/fluent-emoji, MIT licensed)
|
||||
// Covers ZWJ sequences, flags, keycaps, skin tones, text-style emojis, etc.
|
||||
var _emojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfffLine truncated
|
||||
|
||||
function _emojiToCodepoints(emoji) {
|
||||
var codepoints = [];
|
||||
for (var i = 0; i < emoji.length; i++) {
|
||||
var cp = emoji.codePointAt(i);
|
||||
if (cp > 0xFFFF) i++; // Skip low surrogate
|
||||
codepoints.push(cp.toString(16));
|
||||
}
|
||||
return codepoints.join('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Global error handler for fluent emoji images.
|
||||
* Tries alternate codepoint variants (with/without fe0f) before giving up.
|
||||
*/
|
||||
function _fluentEmojiError(img) {
|
||||
var cp = img.dataset.cp;
|
||||
var tried = parseInt(img.dataset.tried || '0');
|
||||
|
||||
if (tried === 0) {
|
||||
// First failure: try toggling fe0f
|
||||
var alt = null;
|
||||
if (cp.indexOf('-fe0f') !== -1) {
|
||||
alt = cp.replace(/-fe0f/g, '');
|
||||
} else {
|
||||
// Try adding fe0f after first codepoint
|
||||
var parts = cp.split('-');
|
||||
parts.splice(1, 0, 'fe0f');
|
||||
alt = parts.join('-');
|
||||
}
|
||||
img.dataset.tried = '1';
|
||||
img.dataset.cp = alt;
|
||||
img.src = FLUENT_EMOJI_CDN + alt + '.webp';
|
||||
return;
|
||||
}
|
||||
|
||||
// All variants failed → replace with original text emoji
|
||||
_emojiFailCache[cp] = true;
|
||||
var text = document.createTextNode(img.alt);
|
||||
img.parentNode.replaceChild(text, img);
|
||||
}
|
||||
|
||||
function replaceEmojisWithFluent(html) {
|
||||
return html.replace(_emojiRegex, function(match) {
|
||||
var codepoints = _emojiToCodepoints(match);
|
||||
if (_emojiFailCache[codepoints]) return match;
|
||||
var url = FLUENT_EMOJI_CDN + codepoints + '.webp';
|
||||
return '<img class="fluent-emoji" src="' + url + '" alt="' + match + '" data-cp="' + codepoints + '" data-tried="0" loading="lazy" draggable="false" onerror="_fluentEmojiError(this)">';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM-based emoji replacement.
|
||||
* Walks all text nodes inside the given element and replaces Unicode emojis
|
||||
* with Fluent Emoji <img> tags. Useful as a post-processing step after
|
||||
* innerHTML has been set (catches emojis that the string-based replace missed).
|
||||
*/
|
||||
function parseFluentEmojis(container) {
|
||||
if (!container) return;
|
||||
var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
|
||||
var textNodes = [];
|
||||
var node;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (_emojiRegex.test(node.nodeValue)) {
|
||||
textNodes.push(node);
|
||||
}
|
||||
// Reset regex lastIndex (global flag not set, but safety)
|
||||
_emojiRegex.lastIndex = 0;
|
||||
}
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
var tn = textNodes[i];
|
||||
// Skip text nodes inside <code>, <pre>, <script>, <style>
|
||||
var parent = tn.parentNode;
|
||||
if (parent && /^(CODE|PRE|SCRIPT|STYLE)$/i.test(parent.tagName)) continue;
|
||||
var replaced = tn.nodeValue.replace(_emojiRegex, function(match) {
|
||||
var codepoints = _emojiToCodepoints(match);
|
||||
if (_emojiFailCache[codepoints]) return match;
|
||||
var url = FLUENT_EMOJI_CDN + codepoints + '.webp';
|
||||
return '<img class="fluent-emoji" src="' + url + '" alt="' + match + '" data-cp="' + codepoints + '" data-tried="0" loading="lazy" draggable="false" onerror="_fluentEmojiError(this)">';
|
||||
});
|
||||
if (replaced !== tn.nodeValue) {
|
||||
var span = document.createElement('span');
|
||||
span.innerHTML = replaced;
|
||||
while (span.firstChild) {
|
||||
parent.insertBefore(span.firstChild, tn);
|
||||
}
|
||||
parent.removeChild(tn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ function _renderMessagesIntoFragment(fragment, msgs) {
|
||||
switch (msg.type) {
|
||||
case 'text':
|
||||
if (msg.role === 'user') {
|
||||
div.className = 'msg user' + (msg.isMarked ? ' marked' : '');
|
||||
div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : '');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
@@ -171,6 +171,7 @@ function _renderMessagesIntoFragment(fragment, msgs) {
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
parseFluentEmojis(div);
|
||||
_addReplyButton(div);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
fragment.appendChild(div);
|
||||
@@ -295,11 +296,12 @@ async function loadLatestArchive() {
|
||||
switch (msg.type) {
|
||||
case 'text':
|
||||
if (msg.role === 'user') {
|
||||
div.className = 'msg user archived';
|
||||
div.className = 'msg user markdown archived';
|
||||
renderUserContent(div, msg.content);
|
||||
} else {
|
||||
div.className = 'msg assistant markdown archived';
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
parseFluentEmojis(div);
|
||||
}
|
||||
if (msg.role === 'user' && msg.attachmentCount > 0) {
|
||||
let names = [];
|
||||
|
||||
+35
-30
@@ -43,6 +43,8 @@ function renderMarkdown(text) {
|
||||
+ '<img class="krea-thumb" src="' + thumbUrl + '" alt="' + safeFn + '" loading="lazy">'
|
||||
+ '</span>';
|
||||
});
|
||||
// Unicode-Emojis durch Fluent Emoji 3D ersetzen (außerhalb von Code-Blöcken)
|
||||
html = replaceEmojisWithFluent(html);
|
||||
// Code-Blöcke wiederherstellen
|
||||
html = html.replace(/%%CODE(\d+)%%/g, function(m, idx) {
|
||||
return codeBlocks[parseInt(idx)];
|
||||
@@ -200,47 +202,47 @@ function renderUserContent(container, text) {
|
||||
}
|
||||
|
||||
function _renderUserTextContent(container, raw) {
|
||||
// [Datei: path] oder [Datei: path:1-10] erkennen
|
||||
// [Datei: path] oder [Datei: path:1-10] durch Platzhalter ersetzen (vor Markdown-Parsing)
|
||||
const pattern = /\[Datei: ([^\]]+)\]/g;
|
||||
let last = 0;
|
||||
let match;
|
||||
const frag = document.createDocumentFragment();
|
||||
const fileRefs = [];
|
||||
const mdText = raw.replace(pattern, function(m, ref) {
|
||||
fileRefs.push(ref);
|
||||
return '%%FILEREF' + (fileRefs.length - 1) + '%%';
|
||||
});
|
||||
|
||||
while ((match = pattern.exec(raw)) !== null) {
|
||||
// Text vor dem Tag
|
||||
if (match.index > last) {
|
||||
frag.appendChild(document.createTextNode(raw.slice(last, match.index)));
|
||||
}
|
||||
// Markdown rendern
|
||||
let html = renderMarkdown(mdText);
|
||||
|
||||
const ref = match[1]; // z.B. "src/foo.cs:10-25"
|
||||
// Pfad ohne Zeilenangabe extrahieren
|
||||
// Platzhalter durch File-Ref-Chips ersetzen (als HTML)
|
||||
html = html.replace(/%%FILEREF(\d+)%%/g, function(m, idx) {
|
||||
const ref = fileRefs[parseInt(idx)];
|
||||
const filePath = ref.replace(/:\d+(-\d+)?$/, '');
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'file-ref-chip';
|
||||
// Schöne Darstellung: Icon + Kurzpfad
|
||||
const fileName = filePath.includes('/') ? filePath.substring(filePath.lastIndexOf('/') + 1) : filePath;
|
||||
const lineInfo = ref.replace(filePath, '').trim(); // z.B. ":10-25"
|
||||
chip.innerHTML = '<i class="fa fa-file-text-o"></i> ' + esc(fileName + lineInfo);
|
||||
chip.title = ref;
|
||||
chip.style.cursor = 'pointer';
|
||||
chip.addEventListener('click', () => {
|
||||
openFileExplorer({ sessionId: getCurrentSessionId(), initialFile: filePath });
|
||||
const lineInfo = ref.replace(filePath, '').trim();
|
||||
return '<span class="file-ref-chip" data-filepath="' + esc(filePath) + '" title="' + esc(ref) + '" style="cursor:pointer">'
|
||||
+ '<i class="fa fa-file-text-o"></i> ' + esc(fileName + lineInfo) + '</span>';
|
||||
});
|
||||
|
||||
// In ein Wrapper-Div rendern (damit vorherige Elemente wie Reply-Quotes erhalten bleiben)
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'user-text-content';
|
||||
wrap.innerHTML = html;
|
||||
container.appendChild(wrap);
|
||||
|
||||
// Click-Handler für File-Ref-Chips nachträglich binden
|
||||
wrap.querySelectorAll('.file-ref-chip[data-filepath]').forEach(function(chip) {
|
||||
chip.addEventListener('click', function() {
|
||||
openFileExplorer({ sessionId: getCurrentSessionId(), initialFile: chip.dataset.filepath });
|
||||
});
|
||||
frag.appendChild(chip);
|
||||
last = match.index + match[0].length;
|
||||
}
|
||||
});
|
||||
|
||||
// Resttext
|
||||
if (last < raw.length) {
|
||||
frag.appendChild(document.createTextNode(raw.slice(last)));
|
||||
}
|
||||
|
||||
container.appendChild(frag);
|
||||
// Fluent Emoji DOM-Parsing
|
||||
parseFluentEmojis(wrap);
|
||||
}
|
||||
|
||||
function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg user' + (isMarked ? ' marked' : '');
|
||||
div.className = 'msg user markdown' + (isMarked ? ' marked' : '');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = timestamp || new Date().toISOString();
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
@@ -259,6 +261,7 @@ function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = f
|
||||
div.dataset.timestamp = timestamp || new Date().toISOString();
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
div.innerHTML = renderMarkdown(text);
|
||||
parseFluentEmojis(div);
|
||||
wrapKreaGalleries(div);
|
||||
_addReplyButton(div);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
@@ -324,6 +327,7 @@ function endStreamBubble() {
|
||||
if (!_streamText.trim()) _streamDiv.remove();
|
||||
else {
|
||||
_streamDiv.innerHTML = renderMarkdown(_streamText);
|
||||
parseFluentEmojis(_streamDiv);
|
||||
wrapKreaGalleries(_streamDiv);
|
||||
}
|
||||
}
|
||||
@@ -645,6 +649,7 @@ function _renderSystemPromptBlocks(container, blocks) {
|
||||
var content = document.createElement('div');
|
||||
content.className = 'sp-block-content markdown';
|
||||
content.innerHTML = renderMarkdown(block.content);
|
||||
parseFluentEmojis(content);
|
||||
wrapper.appendChild(content);
|
||||
|
||||
container.appendChild(wrapper);
|
||||
|
||||
Reference in new issue
Block a user