/// <summary>
        /// Спойлеры
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        public static string MarkSpoilers(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            HashSet <string> usedEmbedsUrls
            )
        {
            text = rSpoilerMark.Replace(text, m1 =>
            {
                var subBlock = RenderMarkdownBlockAsHTMLInnerBlock(
                    m1.Groups[1].Value,
                    chatOption,
                    waitDictionary,
                    guild,
                    mentions,
                    usedEmbedsUrls
                    );

                var wait             = GetWaitString();
                waitDictionary[wait] = string.Format(
                    "<span class='spoiler {1}'><span class='spoiler-content'>{0}</span></span>",
                    subBlock,
                    (chatOption.text_spoiler == 1) ? "spoiler-show" : ""
                    );

                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Жирный наклонный текст
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        public static string MarkBoldItalic(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            HashSet <string> usedEmbedsUrls
            )
        {
            text = rBoldItalic.Replace(text, m1 =>
            {
                var subBlock = RenderMarkdownBlockAsHTMLInnerBlock(
                    m1.Groups[1].Value,
                    chatOption,
                    waitDictionary,
                    guild,
                    mentions,
                    usedEmbedsUrls
                    );

                var wait             = GetWaitString();
                waitDictionary[wait] = string.Format("<strong><em>{0}</em></strong>", subBlock);
                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Упоминание в тексте целой роли с сервера
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <returns></returns>
        public static string MarkMentionsRole(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild
            )
        {
            text = rMentionRole.Replace(text, m1 =>
            {
                string roleID = m1.Groups[1].Value;

                //
                Role role = guild.roles.FirstOrDefault(t => t.id == roleID);
                if (role == null)
                {
                    return(string.Format("<Role Unknown #{0}>", roleID));
                }

                string nickColor = role.color.ToString("X");
                nickColor        = "#" + nickColor.PadLeft(6, '0');
                var wait         = GetWaitString();

                waitDictionary[wait] = string.Format("<span class='role mention' style='color: {1};'>@{0}</span>",
                                                     HttpUtility.HtmlEncode(role.name),
                                                     nickColor
                                                     );

                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Упоминание в сообщении конкретного человека
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <returns></returns>
        public static string MarkMentionsPeople(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions
            )
        {
            // Упоминание человека
            text = rMentionNick.Replace(text, m1 =>
            {
                string mentionID = m1.Groups[1].Value;
                if (mentions == null)
                {
                    // Приколы дискорда
                    return(string.Format("<Пользователь Unknown #{0}>", mentionID));
                }

                EventMessageCreate.EventMessageCreate_Mention mention =
                    mentions.FirstOrDefault(eMention => eMention.id == mentionID);

                if (mention == null)
                {
                    return(string.Format("<Пользователь Unknown #{0}>", mentionID));
                }

                // Выбираем наиболее приоритетную роль
                var nickColor = "inherit";
                if ((guild.roles != null) && (mention.member?.roles != null) &&
                    (chatOption.message_mentions_style == 1))
                {
                    var mention_roles_local =
                        guild.roles.Where(t => mention.member.roles.Contains(t.id)).ToList();
                    mention_roles_local.Sort((a, b) => b.position.CompareTo(a.position));
                    var role = mention_roles_local.Any() ? mention_roles_local.First() : null;
                    if (role != null)
                    {
                        nickColor = role.color.ToString("X");
                        nickColor = "#" + nickColor.PadLeft(6, '0');
                    }
                }
                //

                var wait = GetWaitString();

                waitDictionary[wait] = string.Format("<span class='user mention' {1}>@{0}</span>",
                                                     HttpUtility.HtmlEncode(mention.member?.nick ?? mention.username),
                                                     (chatOption.message_mentions_style == 1) ? string.Format(" style='color: {0};'", nickColor) : ""
                                                     );

                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Emoji (картинки)
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <returns></returns>
        public static string MarkEmojiImages(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild
            )
        {
            var thisGuildEmojis = new HashSet <string>();

            foreach (var emoji in guild.emojis)
            {
                thisGuildEmojis.Add(emoji.id);
            }

            // Эмодзи внутри текста
            text = rEmojiWithinText.Replace(text, m1 =>
            {
                string emojiID  = m1.Groups[3].Value;
                bool isRelative = thisGuildEmojis.Contains(emojiID);
                int emojiShow   = isRelative ? chatOption.emoji_relative : chatOption.emoji_stranger;
                if (emojiShow == 2)
                {
                    return(":" + m1.Groups[2].Value + ":");
                }

                var wait = GetWaitString();
                var url  = string.Format("https://cdn.discordapp.com/emojis/{0}.{1}",
                                         emojiID,
                                         (m1.Groups[1].Value == "a") ? "gif" : "png"
                                         );

                waitDictionary[wait] = string.Format("<span class='emoji {2}'><img src='{0}' alt=':{1}:'></span>",
                                                     HttpUtility.HtmlEncode(url),
                                                     HttpUtility.HtmlEncode(m1.Groups[2].Value),
                                                     (emojiShow == 1) ? "blur" : ""
                                                     );

                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Наклонный (курсивный) текст через звёздочки
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        public static string MarkItalicViaAsterisk(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            HashSet <string> usedEmbedsUrls
            )
        {
            text = rSingleAsterisk.Replace(text, m1 =>
            {
                var firstChar = m1.Groups[1].Value.Substring(0, 1);
                if (firstChar == " ")
                {
                    return(m1.Groups[0].Value);
                }

                var lastChar = m1.Groups[1].Value.Substring(m1.Groups[1].Value.Length - 1);
                if (lastChar == " ")
                {
                    return(m1.Groups[0].Value);
                }

                var subBlock = RenderMarkdownBlockAsHTMLInnerBlock(
                    m1.Groups[1].Value,
                    chatOption,
                    waitDictionary,
                    guild,
                    mentions,
                    usedEmbedsUrls
                    );

                var wait             = GetWaitString();
                waitDictionary[wait] = string.Format("<em>{0}</em>", subBlock);
                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Обработка разметки сообщения внутри логического блока сообщения (root-сообщение, цитата, спойлер)
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        private static string RenderMarkdownBlockAsHTMLInnerBlock(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            HashSet <string> usedEmbedsUrls
            )
        {
            while (true)
            {
                var highlights = new List <dynamic>();
                foreach (var pair in new List <dynamic>()
                {
                    new
                    {
                        regexp = rWithoutMark,
                        deleg = (Func <string>)(() => MarkNoFormatting(text, chatOption, waitDictionary))
                    },
                    new
                    {
                        regexp = rLink,
                        deleg = (Func <string>)(() => MarkLinks(text, chatOption, waitDictionary, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rEmojiWithinText,
                        deleg = (Func <string>)(() => MarkEmojiImages(text, chatOption, waitDictionary, guild))
                    },
                    // mentions
                    new
                    {
                        regexp = rMentionNick,
                        deleg = (Func <string>)(() =>
                                                MarkMentionsPeople(text, chatOption, waitDictionary, guild, mentions))
                    },
                    new
                    {
                        regexp = rMentionRole,
                        deleg = (Func <string>)(() => MarkMentionsRole(text, chatOption, waitDictionary, guild))
                    },

                    // simple mark
                    new
                    {
                        regexp = rSpoilerMark,
                        deleg = (Func <string>)(() =>
                                                MarkSpoilers(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rBoldItalic,
                        deleg = (Func <string>)(() =>
                                                MarkBoldItalic(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rBold,
                        deleg = (Func <string>)(() =>
                                                MarkBold(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },

                    new
                    {
                        regexp = rTripleUnderscore,
                        deleg = (Func <string>)(() =>
                                                MarkUnderscoreItalic(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rDoubleUnderscore,
                        deleg = (Func <string>)(() =>
                                                MarkUnderscore(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rSingleAsterisk,
                        deleg = (Func <string>)(() =>
                                                MarkItalicViaAsterisk(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rItalicUnderscore,
                        deleg = (Func <string>)(() =>
                                                MarkItalicViaUnderscore(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rDelete,
                        deleg = (Func <string>)(() =>
                                                MarkDelete(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                })
                {
                    Regex regex = pair.regexp;
                    var   m     = regex.Match(text);
                    if (!m.Success)
                    {
                        continue;
                    }

                    highlights.Add(new { index = m.Index, pair.deleg });
                    if (m.Index == 0)
                    {
                        break;
                    }
                }

                if (!highlights.Any())
                {
                    // Не осталось маркировки, выходим

                    break;
                }

                highlights.Sort((a, b) => a.index.CompareTo(b.index));

                var u = false;
                // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
                foreach (var highlight in highlights)
                {
                    Func <string> delegateLocal = highlight.deleg;
                    var           result        = delegateLocal.Invoke();
                    if (result != text)
                    {
                        text = result;
                        u    = true;
                        break;
                    }
                }

                if (!u)
                {
                    // hint: Это warning однозначный
                    break;
                }
            }

            text = MarkEmojiUnicode(text, chatOption, waitDictionary);

            return(text);
        }