1 import { communitySearch, personSearch } from "@utils/app";
2 import { isBrowser } from "@utils/browser";
3 import { debounce, groupBy } from "@utils/helpers";
4 import { CommunityTribute, PersonTribute } from "@utils/types";
5 import { Picker } from "emoji-mart";
6 import emojiShortName from "emoji-short-name";
7 import { CustomEmojiView } from "lemmy-js-client";
8 import { default as MarkdownIt } from "markdown-it";
9 import markdown_it_container from "markdown-it-container";
10 // import markdown_it_emoji from "markdown-it-emoji/bare";
11 import markdown_it_footnote from "markdown-it-footnote";
12 import markdown_it_html5_embed from "markdown-it-html5-embed";
13 import markdown_it_sub from "markdown-it-sub";
14 import markdown_it_sup from "markdown-it-sup";
15 import Renderer from "markdown-it/lib/renderer";
16 import Token from "markdown-it/lib/token";
18 export let Tribute: any;
20 export let md: MarkdownIt = new MarkdownIt();
22 export let mdNoImages: MarkdownIt = new MarkdownIt();
24 export const customEmojis: EmojiMartCategory[] = [];
26 export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
32 Tribute = require("tributejs");
35 export function mdToHtml(text: string) {
36 return { __html: md.render(text) };
39 export function mdToHtmlNoImages(text: string) {
40 return { __html: mdNoImages.render(text) };
43 export function mdToHtmlInline(text: string) {
44 return { __html: md.renderInline(text) };
47 const spoilerConfig = {
48 validate: (params: string) => {
49 return params.trim().match(/^spoiler\s+(.*)$/);
52 render: (tokens: any, idx: any) => {
53 var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
55 if (tokens[idx].nesting === 1) {
57 return `<details><summary> ${md.utils.escapeHtml(m[1])} </summary>\n`;
60 return "</details>\n";
65 const html5EmbedConfig = {
67 useImageSyntax: true, // Enables video/audio embed with ![]() syntax (default)
69 audio: 'controls preload="metadata"',
70 video: 'width="100%" max-height="100%" controls loop preload="metadata"',
75 export function setupMarkdown() {
76 const markdownItConfig: MarkdownIt.Options = {
82 // const emojiDefs = Array.from(customEmojisLookup.entries()).reduce(
83 // (main, [key, value]) => ({ ...main, [key]: value }),
86 md = new MarkdownIt(markdownItConfig)
89 .use(markdown_it_footnote)
90 .use(markdown_it_html5_embed, html5EmbedConfig)
91 .use(markdown_it_container, "spoiler", spoilerConfig);
92 // .use(markdown_it_emoji, {
96 mdNoImages = new MarkdownIt(markdownItConfig)
99 .use(markdown_it_footnote)
100 .use(markdown_it_html5_embed, html5EmbedConfig)
101 .use(markdown_it_container, "spoiler", spoilerConfig)
102 // .use(markdown_it_emoji, {
106 const defaultRenderer = md.renderer.rules.image;
107 md.renderer.rules.image = function (
110 options: MarkdownIt.Options,
114 //Provide custom renderer for our emojis to allow us to add a css class and force size dimensions on them.
115 const item = tokens[idx] as any;
116 const title = item.attrs.length >= 3 ? item.attrs[2][1] : "";
117 const src: string = item.attrs[0][1];
118 const isCustomEmoji = customEmojisLookup.get(title) != undefined;
119 if (!isCustomEmoji) {
120 return defaultRenderer?.(tokens, idx, options, env, self) ?? "";
122 const alt_text = item.content;
123 return `<img class="icon icon-emoji" src="${src}" title="${title}" alt="${alt_text}"/>`;
125 md.renderer.rules.table_open = function () {
126 return '<table class="table">';
130 export function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) {
131 const groupedEmojis = groupBy(
133 x => x.custom_emoji.category
135 for (const [category, emojis] of Object.entries(groupedEmojis)) {
139 emojis: emojis.map(emoji => ({
140 id: emoji.custom_emoji.shortcode,
141 name: emoji.custom_emoji.shortcode,
142 keywords: emoji.keywords.map(x => x.keyword),
143 skins: [{ src: emoji.custom_emoji.image_url }],
147 customEmojisLookup = new Map(
148 custom_emoji_views.map(view => [view.custom_emoji.shortcode, view])
152 export function updateEmojiDataModel(custom_emoji_view: CustomEmojiView) {
153 const emoji: EmojiMartCustomEmoji = {
154 id: custom_emoji_view.custom_emoji.shortcode,
155 name: custom_emoji_view.custom_emoji.shortcode,
156 keywords: custom_emoji_view.keywords.map(x => x.keyword),
157 skins: [{ src: custom_emoji_view.custom_emoji.image_url }],
159 const categoryIndex = customEmojis.findIndex(
160 x => x.id == custom_emoji_view.custom_emoji.category
162 if (categoryIndex == -1) {
164 id: custom_emoji_view.custom_emoji.category,
165 name: custom_emoji_view.custom_emoji.category,
169 const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
170 x => x.id == custom_emoji_view.custom_emoji.shortcode
172 if (emojiIndex == -1) {
173 customEmojis[categoryIndex].emojis.push(emoji);
175 customEmojis[categoryIndex].emojis[emojiIndex] = emoji;
178 customEmojisLookup.set(
179 custom_emoji_view.custom_emoji.shortcode,
184 export function removeFromEmojiDataModel(id: number) {
185 let view: CustomEmojiView | undefined;
186 for (const item of customEmojisLookup.values()) {
187 if (item.custom_emoji.id === id) {
193 const categoryIndex = customEmojis.findIndex(
194 x => x.id == view?.custom_emoji.category
196 const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
197 x => x.id == view?.custom_emoji.shortcode
199 customEmojis[categoryIndex].emojis = customEmojis[
201 ].emojis.splice(emojiIndex, 1);
203 customEmojisLookup.delete(view?.custom_emoji.shortcode);
206 export function getEmojiMart(
207 onEmojiSelect: (e: any) => void,
208 customPickerOptions: any = {}
210 const pickerOptions = {
211 ...customPickerOptions,
212 onEmojiSelect: onEmojiSelect,
213 custom: customEmojis,
215 return new Picker(pickerOptions);
218 export function setupTribute() {
220 noMatchTemplate: function () {
227 menuItemTemplate: (item: any) => {
228 const shortName = `:${item.original.key}:`;
229 return `${item.original.val} ${shortName}`;
231 selectTemplate: (item: any) => {
232 const customEmoji = customEmojisLookup.get(
235 if (customEmoji == undefined) return `${item.original.val}`;
237 return `![${customEmoji.alt_text}](${customEmoji.image_url} "${customEmoji.shortcode}")`;
239 values: Object.entries(emojiShortName)
241 return { key: e[1], val: e[0] };
244 Array.from(customEmojisLookup.entries()).map(k => ({
246 val: `<img class="icon icon-emoji" src="${k[1].custom_emoji.image_url}" title="${k[1].custom_emoji.shortcode}" alt="${k[1].custom_emoji.alt_text}" />`,
250 autocompleteMode: true,
252 // menuItemLimit: mentionDropdownFetchLimit,
253 menuShowMinLength: 2,
258 selectTemplate: (item: any) => {
259 const it: PersonTribute = item.original;
260 return `[${it.key}](${it.view.person.actor_id})`;
262 values: debounce(async (text: string, cb: any) => {
263 cb(await personSearch(text));
266 autocompleteMode: true,
268 // menuItemLimit: mentionDropdownFetchLimit,
269 menuShowMinLength: 2,
275 selectTemplate: (item: any) => {
276 const it: CommunityTribute = item.original;
277 return `[${it.key}](${it.view.community.actor_id})`;
279 values: debounce(async (text: string, cb: any) => {
280 cb(await communitySearch(text));
283 autocompleteMode: true,
285 // menuItemLimit: mentionDropdownFetchLimit,
286 menuShowMinLength: 2,
292 interface EmojiMartCategory {
295 emojis: EmojiMartCustomEmoji[];
298 interface EmojiMartCustomEmoji {
302 skins: EmojiMartSkin[];
305 interface EmojiMartSkin {