mirror of
https://github.com/vadimmelnicuk/meo.git
synced 2026-05-03 12:40:38 +00:00
feat: add emoji support
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"highlight.js": "^11.11.1",
|
||||
"lucide": "^0.564.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-emoji": "^3.0.0",
|
||||
"mermaid": "^11.4.1",
|
||||
"puppeteer-core": "^24.1.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
@@ -399,6 +400,8 @@
|
||||
|
||||
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
|
||||
|
||||
"markdown-it-emoji": ["markdown-it-emoji@3.0.0", "", {}, "sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg=="],
|
||||
|
||||
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"highlight.js": "^11.11.1",
|
||||
"lucide": "^0.564.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-emoji": "^3.0.0",
|
||||
"mermaid": "^11.4.1",
|
||||
"puppeteer-core": "^24.1.1",
|
||||
"sanitize-html": "^2.17.0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { light as emoji } from 'markdown-it-emoji';
|
||||
import hljs from 'highlight.js';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { rewriteExportImageSrc } from './assetPaths';
|
||||
@@ -53,6 +54,7 @@ export function renderMarkdownToHtml(options: RenderMarkdownOptions): RenderMark
|
||||
].join('');
|
||||
}
|
||||
});
|
||||
md.use(emoji);
|
||||
installTaskListTransform(md);
|
||||
|
||||
const defaultImageRule = md.renderer.rules.image ?? ((tokens, idx, opts, _env, self) => self.renderToken(tokens, idx, opts));
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
export const emojiData: Record<string, string> = {
|
||||
smile: '😄', grinning: '😁', joy: '😂', rofl: '🤣', sob: '😭',
|
||||
angry: '😠', rage: '😡',
|
||||
heart: '❤️', pink_heart: '💗', broken_heart: '💔',
|
||||
orange_heart: '🧡', yellow_heart: '💛', green_heart: '💚',
|
||||
blue_heart: '💙', purple_heart: '💜', black_heart: '🖤',
|
||||
white_heart: '🤍', brown_heart: '🤎',
|
||||
star: '⭐', stars: '🌟', sparkles: '✨', zap: '⚡', fire: '🔥',
|
||||
hundred: '💯', check: '✅', x: '❌', warning: '⚠️',
|
||||
plus: '➕', minus: '➖', divided: '➗', multiply: '✖️',
|
||||
arrow_right: '➡️', arrow_left: '⬅️', arrow_up: '⬆️', arrow_down: '⬇️',
|
||||
pencil: '✏️', memo: '📝', page_facing_up: '📄', link: '🔗',
|
||||
paperclip: '📎', scissors: '✂️', bomb: '💣', boom: '💥', dizzy: '💫',
|
||||
droplet: '💧', cloud: '☀️', sunny: '☀️', moon: '🌙', snowflake: '❄️',
|
||||
snowman: '⛄', umbrella: '☔', rainbow: '🌈',
|
||||
earth_americas: '🌎', earth_africa: '🌍', earth_asia: '🌏', globe: '🌐',
|
||||
skull: '💀', alien: '👽', robot: '🤖', ghost: '👻', santa: '🎅',
|
||||
angel: '👼', boy: '👦', girl: '👧', woman: '👩', man: '👨', baby: '👶',
|
||||
thumbsup: '👍', thumbsdown: '👎', punch: '👊', fist: '👊', wave: '👋',
|
||||
raised_hand: '✋', clap: '👏', pray: '🙏', muscle: '💪',
|
||||
ear: '👂', eye: '👁️', eyes: '👀', tongue: '👅', kiss: '💋',
|
||||
cupid: '💘', heart_with_ribbon: '💝', speech_balloon: '💬',
|
||||
thought_balloon: '💭', bell: '🔔', musical_note: '🎵',
|
||||
phone: '📞', envelope: '✉️',
|
||||
pizza: '🍕', hamburger: '🍔', fries: '🍟', spaghetti: '🍝',
|
||||
taco: '🌮', burrito: '🌯', popcorn: '🍿', egg: '🥚', rice: '🍚',
|
||||
sushi: '🍣', bread: '🍞', cake: '🍰', cookie: '🍪', candy: '🍬',
|
||||
lollipop: '🍭', coffee: '☕', tea: '☕', beer: '🍺', beers: '🍻',
|
||||
wine_glass: '🍷', cocktail: '🍸', tropical_drink: '🍹',
|
||||
strawberry: '🍓', grapes: '🍇', watermelon: '🍉', lemon: '🍋',
|
||||
banana: '🍌', apple: '🍎', cherry: '🍒', tomato: '🍅', corn: '🌽',
|
||||
house: '🏠', school: '🏫', hospital: '🏥', bank: '🏦', hotel: '🏨',
|
||||
church: '⛪', mosque: '🕌', fountain: '⛲', tent: '⛺',
|
||||
car: '🚗', taxi: '🚕', bus: '🚌', train: '🚃', airplane: '✈️',
|
||||
boat: '⛵', ship: '🚢', ambulance: '🚑', fire_engine: '🚒',
|
||||
police_car: '🚓', trophy: '🏆', medal: '🏅', flag: '🏴',
|
||||
sun: '☀️', mountain: '⛰️', volcano: '🌋', beach: '🏖️',
|
||||
palm_tree: '🌴', cactus: '🌵', flower: '🌸', rose: '🌹', tulip: '🌷',
|
||||
dog: '🐶', cat: '🐱', mouse: '🐭', rabbit: '🐰', bear: '🐻',
|
||||
panda: '🐼', koala: '🐨', tiger: '🐯', lion: '🦁', cow: '🐮',
|
||||
pig: '🐷', frog: '🐸', monkey: '🐵', chicken: '🐔', penguin: '🐧',
|
||||
bird: '🐦', eagle: '🦅', owl: '🦉', wolf: '🐺', bee: '🐝',
|
||||
bug: '🐛', snail: '🐌', butterfly: '🦋', fish: '🐟', dolphin: '🐬',
|
||||
whale: '🐳', octopus: '🐙', dragon: '🐉', snake: '🐍', horse: '🐴',
|
||||
unicorn: '🦄', bull: '🐂', ram: '🐏', zebra: '🦓', deer: '🦌',
|
||||
camel: '🐪', elephant: '🐘',
|
||||
horse_racing: '🏇', skiing: '⛷️', surfing: '🏄', swimming: '🏊',
|
||||
fishing: '🎣', boxing: '🥊', basketball: '🏀', football: '🏈',
|
||||
baseball: '⚾', soccer: '⚽', tennis: '🎾', volleyball: '🏐',
|
||||
golf: '⛳', running: '🏃', walking: '🚶', dancing: '💃',
|
||||
cyclist: '🚴', person_lifting: '🏋️', bath: '🛀', sleeping: '😴',
|
||||
toilet: '🚽', shower: '🚿', bedroom: '🛏️', couch: '🛋️',
|
||||
clock: '🕐', alarm: '⏰', hourglass: '⏳', calendar: '📅', watch: '⌚',
|
||||
key: '🔑', lock: '🔒', unlock: '🔓', bulb: '💡', flashlight: '🔦',
|
||||
candle: '🕯️', money: '💵', credit_card: '💳', gift: '🎁',
|
||||
balloon: '🎈', confetti: '🎊', tada: '🎉', movie: '🎬', game: '🎮',
|
||||
dice: '🎲', chess: '♟️', guitar: '🎸', piano: '🎹', violin: '🎻',
|
||||
trumpet: '🎺', saxophone: '🎷', drum: '🥁', art: '🎨',
|
||||
camera: '📷', video: '📹', tv: '📺', smartphone: '📱', computer: '💻',
|
||||
keyboard: '⌨️', floppy_disk: '💾', cd: '💿', dvd: '📀',
|
||||
microscope: '🔬', telescope: '🔭', book: '📖', books: '📚',
|
||||
newspaper: '📰', notebook: '📓', pen: '🖊️', money_with_wings: '💸',
|
||||
wc: '🚾', wheelchair: '♿', no_smoking: '🚭', potable_water: '🚰',
|
||||
mens: '🚹', womens: '🚺', restroom: '🚻', baby_symbol: '🚼',
|
||||
door: '🚪', coin: '🪙', yen: '💴',
|
||||
keycap_0: '0️⃣', keycap_1: '1️⃣', keycap_2: '2️⃣', keycap_3: '3️⃣',
|
||||
keycap_4: '4️⃣', keycap_5: '5️⃣', keycap_6: '6️⃣', keycap_7: '7️⃣',
|
||||
keycap_8: '8️⃣', keycap_9: '9️⃣', keycap_10: '🔟',
|
||||
hash: '#️⃣', asterisk: '*️⃣',
|
||||
a: '🅰️', b: '🅱️', o: '⭕', o2: '🅾️',
|
||||
negative_squared_cross_mark: '❎', white_check_mark: '✅',
|
||||
black_square_button: '🔲', white_square_button: '🔳',
|
||||
diamond_shape_with_a_dot_inside: '💠',
|
||||
black_medium_square: '◼️', white_medium_square: '◻️',
|
||||
black_medium_small_square: '◾', white_medium_small_square: '◽',
|
||||
black_small_square: '▪️', white_small_square: '▫️',
|
||||
large_red_circle: '🔴', large_blue_circle: '🔵',
|
||||
large_orange_diamond: '🔶', large_blue_diamond: '🔷',
|
||||
small_orange_diamond: '🔸', small_blue_diamond: '🔹',
|
||||
red_triangle_pointed_down: '🔻', red_triangle_pointed_up: '🔺',
|
||||
diamond: '💎', checkered_flag: '🏁',
|
||||
flag_us: '🇺🇸', flag_gb: '🇬🇧', flag_ca: '🇨🇦', flag_au: '🇦🇺',
|
||||
flag_de: '🇩🇪', flag_fr: '🇫🇷', flag_it: '🇮🇹', flag_jp: '🇯🇵',
|
||||
flag_kr: '🇰🇷', flag_cn: '🇨🇳', flag_br: '🇧🇷', flag_in: '🇮🇳',
|
||||
flag_mx: '🇲🇽', flag_es: '🇪🇸', flag_ru: '🇷🇺', flag_za: '🇿🇦',
|
||||
flag_ae: '🇦🇪', flag_sg: '🇸🇬', flag_hk: '🇭🇰', flag_tw: '🇹🇼',
|
||||
flag_nl: '🇳🇱', flag_se: '🇸🇪', flag_no: '🇳🇴', flag_dk: '🇩🇰',
|
||||
flag_fi: '🇫🇮', flag_ch: '🇨🇭', flag_be: '🇧🇪', flag_at: '🇦🇹',
|
||||
flag_pt: '🇵🇹', flag_pl: '🇵🇱', flag_gr: '🇬🇷', flag_cz: '🇨🇿',
|
||||
flag_ro: '🇷🇴', flag_th: '🇹🇭', flag_vn: '🇻🇳', flag_id: '🇮🇩',
|
||||
flag_my: '🇲🇾', flag_ph: '🇵🇭', flag_tr: '🇹🇷', flag_il: '🇮🇱',
|
||||
flag_eg: '🇪🇬', flag_ua: '🇺🇦', flag_ar: '🇦🇷'
|
||||
};
|
||||
|
||||
export interface EmojiRange {
|
||||
emoji: string;
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
const emojiPattern = /:([a-zA-Z0-9_+-]+):/g;
|
||||
|
||||
export function collectEmojiRangesFromText(text: string, lineFrom: number): EmojiRange[] {
|
||||
const ranges: EmojiRange[] = [];
|
||||
|
||||
emojiPattern.lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = emojiPattern.exec(text)) !== null) {
|
||||
const emoji = emojiData[match[1]];
|
||||
if (emoji) {
|
||||
ranges.push({
|
||||
emoji,
|
||||
from: lineFrom + match.index,
|
||||
to: lineFrom + match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { ImageWidget, getImageData, isImageUrl } from './helpers/images';
|
||||
import { highlightStyle } from './theme';
|
||||
import { collectSingleTildeStrikePairs, collectStrikethroughRanges } from './helpers/strikeMarkers';
|
||||
import { collectEmojiRangesFromText } from './helpers/emoji';
|
||||
import { headingLevelFromName, resolvedSyntaxTree } from './helpers/markdownSyntax';
|
||||
import {
|
||||
getCollapsedHeadingSections,
|
||||
@@ -843,6 +844,7 @@ function buildDecorations(state) {
|
||||
addFallbackTableDecorations(ranges, state, tree, parsedTableRanges);
|
||||
addSingleTildeStrikeDecorations(ranges, state, activeLines, strikeRanges, codeBlockLines);
|
||||
addListLineDecorations(ranges, state, indentSelectedLines, frontmatter, codeBlockLines);
|
||||
addEmojiDecorations(ranges, state, codeBlockLines);
|
||||
for (const section of collapsedHeadingSections) {
|
||||
addLineClass(ranges, state, section.lineFrom, section.lineTo, collapsedHeadingLineDeco);
|
||||
addRange(ranges, section.collapseFrom, section.collapseTo, collapsedHeadingBodyDeco);
|
||||
@@ -940,6 +942,51 @@ function collectCodeBlockLines(state, tree) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
const emojiWidgetCache = new Map<string, WidgetType>();
|
||||
|
||||
function getEmojiWidget(emoji: string): WidgetType {
|
||||
let widget = emojiWidgetCache.get(emoji);
|
||||
if (!widget) {
|
||||
widget = new (class extends WidgetType {
|
||||
toDOM() {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'meo-md-emoji';
|
||||
span.textContent = emoji;
|
||||
return span;
|
||||
}
|
||||
ignoreEvent() {
|
||||
return true;
|
||||
}
|
||||
})();
|
||||
emojiWidgetCache.set(emoji, widget);
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
function addEmojiDecorations(builder, state, codeBlockLines = null) {
|
||||
for (let lineNo = 1; lineNo <= state.doc.lines; lineNo += 1) {
|
||||
if (codeBlockLines?.has(lineNo)) {
|
||||
continue;
|
||||
}
|
||||
const line = state.doc.line(lineNo);
|
||||
const lineText = state.doc.sliceString(line.from, line.to);
|
||||
|
||||
if (lineText.indexOf(':') === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const emojiRanges = collectEmojiRangesFromText(lineText, line.from);
|
||||
for (const emojiRange of emojiRanges) {
|
||||
builder.push(
|
||||
Decoration.replace({
|
||||
widget: getEmojiWidget(emojiRange.emoji),
|
||||
inclusive: false
|
||||
}).range(emojiRange.from, emojiRange.to)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const liveDecorationField = StateField.define({
|
||||
create(state) {
|
||||
return safeBuildDecorations(state, Decoration.none, 'create');
|
||||
|
||||
Reference in New Issue
Block a user