Skip to content

Commit ad2ea3e

Browse files
Merge pull request #89 from ThisIs-Developer/copilot/fix-emoji-rendering-in-markdown
Render GitHub emoji shortcodes missing from JoyPixels
2 parents 4a18e3f + 560884e commit ad2ea3e

4 files changed

Lines changed: 119 additions & 18 deletions

File tree

desktop-app/resources/js/script.js

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ document.addEventListener("DOMContentLoaded", function () {
8282
const EMOJI_API_URL = 'https://api.github.com/emojis';
8383
let emojiLoadPromise = null;
8484
let emojiEntries = [];
85+
let emojiUrlMap = new Map();
86+
let emojiLookupLoaded = false;
87+
let emojiRenderScheduled = false;
8588
let emojiItems = [];
8689
const emojiSelection = new Set();
8790
let symbolItems = [];
@@ -1493,6 +1496,20 @@ This is a fully client-side application. Your content never leaves your browser
14931496
}
14941497
}
14951498

1499+
function scheduleEmojiLookupRefresh() {
1500+
if (emojiLookupLoaded || emojiRenderScheduled) return;
1501+
emojiRenderScheduled = true;
1502+
loadEmojiEntries()
1503+
.then(() => {
1504+
if (emojiUrlMap.size) {
1505+
renderMarkdown();
1506+
}
1507+
})
1508+
.finally(() => {
1509+
emojiRenderScheduled = false;
1510+
});
1511+
}
1512+
14961513
function processEmojis(element) {
14971514
const walker = document.createTreeWalker(
14981515
element,
@@ -1519,36 +1536,59 @@ This is a fully client-side application. Your content never leaves your browser
15191536
}
15201537
}
15211538

1539+
let needsEmojiLookup = false;
15221540
textNodes.forEach(textNode => {
15231541
const text = textNode.nodeValue;
15241542
const emojiRegex = /:([\w+-]+):/g;
15251543

15261544
let match;
15271545
let lastIndex = 0;
1528-
let result = '';
15291546
let hasEmoji = false;
1547+
const fragment = document.createDocumentFragment();
15301548

15311549
while ((match = emojiRegex.exec(text)) !== null) {
15321550
const shortcode = match[1];
15331551
const emoji = joypixels.shortnameToUnicode(`:${shortcode}:`);
15341552

15351553
if (emoji !== `:${shortcode}:`) { // If conversion was successful
15361554
hasEmoji = true;
1537-
result += text.substring(lastIndex, match.index) + emoji;
1555+
if (match.index > lastIndex) {
1556+
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
1557+
}
1558+
fragment.appendChild(document.createTextNode(emoji));
15381559
lastIndex = emojiRegex.lastIndex;
15391560
} else {
1540-
result += text.substring(lastIndex, emojiRegex.lastIndex);
1541-
lastIndex = emojiRegex.lastIndex;
1561+
const emojiUrl = emojiUrlMap.get(shortcode);
1562+
if (emojiUrl) {
1563+
hasEmoji = true;
1564+
if (match.index > lastIndex) {
1565+
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
1566+
}
1567+
const image = document.createElement('img');
1568+
image.className = 'emoji-inline';
1569+
image.src = emojiUrl;
1570+
image.alt = `:${shortcode}:`;
1571+
image.loading = 'lazy';
1572+
image.setAttribute('aria-label', `:${shortcode}:`);
1573+
fragment.appendChild(image);
1574+
lastIndex = emojiRegex.lastIndex;
1575+
} else if (!emojiLookupLoaded) {
1576+
needsEmojiLookup = true;
1577+
}
15421578
}
15431579
}
15441580

15451581
if (hasEmoji) {
1546-
result += text.substring(lastIndex);
1547-
const span = document.createElement('span');
1548-
span.innerHTML = result;
1549-
textNode.parentNode.replaceChild(span, textNode);
1582+
if (lastIndex < text.length) {
1583+
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
1584+
}
1585+
textNode.parentNode.replaceChild(fragment, textNode);
15501586
}
15511587
});
1588+
1589+
if (needsEmojiLookup) {
1590+
scheduleEmojiLookupRefresh();
1591+
}
15521592
}
15531593

15541594
function debouncedRender() {
@@ -2115,11 +2155,15 @@ This is a fully client-side application. Your content never leaves your browser
21152155
shortcode: `:${name}:`,
21162156
search: `${name} :${name}:`.toLowerCase(),
21172157
}));
2158+
emojiUrlMap = new Map(emojiEntries.map((entry) => [entry.name, entry.url]));
2159+
emojiLookupLoaded = true;
21182160
return emojiEntries;
21192161
})
21202162
.catch((error) => {
21212163
console.error('Failed to load GitHub emojis:', error);
21222164
emojiEntries = [];
2165+
emojiUrlMap = new Map();
2166+
emojiLookupLoaded = true;
21232167
return emojiEntries;
21242168
});
21252169
return emojiLoadPromise;
@@ -2511,7 +2555,8 @@ This is a fully client-side application. Your content never leaves your browser
25112555

25122556
const alertTypes = ['note', 'tip', 'important', 'warning', 'caution'];
25132557
let selectedType = alertTypes[0];
2514-
const options = alertTypes.map((type) => {
2558+
const options = [];
2559+
alertTypes.forEach((type) => {
25152560
const meta = GITHUB_ALERT_META[type] || { label: type };
25162561
const option = document.createElement('button');
25172562
option.type = 'button';
@@ -2531,8 +2576,8 @@ This is a fully client-side application. Your content never leaves your browser
25312576
item.setAttribute('aria-pressed', isSelected.toString());
25322577
});
25332578
});
2579+
options.push(option);
25342580
grid.appendChild(option);
2535-
return option;
25362581
});
25372582

25382583
function insertAlert() {

desktop-app/resources/styles.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ body {
182182
padding: 0.2em 0.4em;
183183
}
184184

185+
.markdown-body img.emoji-inline {
186+
width: 1em;
187+
height: 1em;
188+
vertical-align: -0.1em;
189+
}
190+
185191
.markdown-body .markdown-alert {
186192
padding: 0.5rem 1rem;
187193
margin-bottom: 16px;

script.js

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ document.addEventListener("DOMContentLoaded", function () {
8282
const EMOJI_API_URL = 'https://api.github.com/emojis';
8383
let emojiLoadPromise = null;
8484
let emojiEntries = [];
85+
let emojiUrlMap = new Map();
86+
let emojiLookupLoaded = false;
87+
let emojiRenderScheduled = false;
8588
let emojiItems = [];
8689
const emojiSelection = new Set();
8790
let symbolItems = [];
@@ -1493,6 +1496,20 @@ This is a fully client-side application. Your content never leaves your browser
14931496
}
14941497
}
14951498

1499+
function scheduleEmojiLookupRefresh() {
1500+
if (emojiLookupLoaded || emojiRenderScheduled) return;
1501+
emojiRenderScheduled = true;
1502+
loadEmojiEntries()
1503+
.then(() => {
1504+
if (emojiUrlMap.size) {
1505+
renderMarkdown();
1506+
}
1507+
})
1508+
.finally(() => {
1509+
emojiRenderScheduled = false;
1510+
});
1511+
}
1512+
14961513
function processEmojis(element) {
14971514
const walker = document.createTreeWalker(
14981515
element,
@@ -1519,36 +1536,59 @@ This is a fully client-side application. Your content never leaves your browser
15191536
}
15201537
}
15211538

1539+
let needsEmojiLookup = false;
15221540
textNodes.forEach(textNode => {
15231541
const text = textNode.nodeValue;
15241542
const emojiRegex = /:([\w+-]+):/g;
15251543

15261544
let match;
15271545
let lastIndex = 0;
1528-
let result = '';
15291546
let hasEmoji = false;
1547+
const fragment = document.createDocumentFragment();
15301548

15311549
while ((match = emojiRegex.exec(text)) !== null) {
15321550
const shortcode = match[1];
15331551
const emoji = joypixels.shortnameToUnicode(`:${shortcode}:`);
15341552

15351553
if (emoji !== `:${shortcode}:`) { // If conversion was successful
15361554
hasEmoji = true;
1537-
result += text.substring(lastIndex, match.index) + emoji;
1555+
if (match.index > lastIndex) {
1556+
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
1557+
}
1558+
fragment.appendChild(document.createTextNode(emoji));
15381559
lastIndex = emojiRegex.lastIndex;
15391560
} else {
1540-
result += text.substring(lastIndex, emojiRegex.lastIndex);
1541-
lastIndex = emojiRegex.lastIndex;
1561+
const emojiUrl = emojiUrlMap.get(shortcode);
1562+
if (emojiUrl) {
1563+
hasEmoji = true;
1564+
if (match.index > lastIndex) {
1565+
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
1566+
}
1567+
const image = document.createElement('img');
1568+
image.className = 'emoji-inline';
1569+
image.src = emojiUrl;
1570+
image.alt = `:${shortcode}:`;
1571+
image.loading = 'lazy';
1572+
image.setAttribute('aria-label', `:${shortcode}:`);
1573+
fragment.appendChild(image);
1574+
lastIndex = emojiRegex.lastIndex;
1575+
} else if (!emojiLookupLoaded) {
1576+
needsEmojiLookup = true;
1577+
}
15421578
}
15431579
}
15441580

15451581
if (hasEmoji) {
1546-
result += text.substring(lastIndex);
1547-
const span = document.createElement('span');
1548-
span.innerHTML = result;
1549-
textNode.parentNode.replaceChild(span, textNode);
1582+
if (lastIndex < text.length) {
1583+
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
1584+
}
1585+
textNode.parentNode.replaceChild(fragment, textNode);
15501586
}
15511587
});
1588+
1589+
if (needsEmojiLookup) {
1590+
scheduleEmojiLookupRefresh();
1591+
}
15521592
}
15531593

15541594
function debouncedRender() {
@@ -2115,11 +2155,15 @@ This is a fully client-side application. Your content never leaves your browser
21152155
shortcode: `:${name}:`,
21162156
search: `${name} :${name}:`.toLowerCase(),
21172157
}));
2158+
emojiUrlMap = new Map(emojiEntries.map((entry) => [entry.name, entry.url]));
2159+
emojiLookupLoaded = true;
21182160
return emojiEntries;
21192161
})
21202162
.catch((error) => {
21212163
console.error('Failed to load GitHub emojis:', error);
21222164
emojiEntries = [];
2165+
emojiUrlMap = new Map();
2166+
emojiLookupLoaded = true;
21232167
return emojiEntries;
21242168
});
21252169
return emojiLoadPromise;

styles.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ body {
182182
padding: 0.2em 0.4em;
183183
}
184184

185+
.markdown-body img.emoji-inline {
186+
width: 1em;
187+
height: 1em;
188+
vertical-align: -0.1em;
189+
}
190+
185191
.markdown-body .markdown-alert {
186192
padding: 0.5rem 1rem;
187193
margin-bottom: 16px;

0 commit comments

Comments
 (0)