+++ /dev/null
-// OpenTween - Client of Twitter
-// Copyright (c) 2015 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
-// All rights reserved.
-//
-// This file is part of OpenTween.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-// for more details.
-//
-// You should have received a copy of the GNU General Public License along
-// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
-// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
-// Boston, MA 02110-1301, USA.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Xunit;
-
-namespace OpenTween
-{
- public class EmojiFormatterTest
- {
- [Fact]
- public void ReplaceEmojiToImg_Test()
- {
- var origText = "©"; // U+00A9
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/a9.png\" alt=\"©\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_VariationSelector_TextStyleTest()
- {
- // 異字体セレクタを使用して明示的にテキストスタイルで表示させている文字
- var origText = "©\uFE0E"; // U+00A9 + U+FE0E (text style)
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "©\uFE0E";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_VariationSelector_EmojiStyleTest()
- {
- // 異字体セレクタを使用して明示的に絵文字スタイルで表示させている文字
- var origText = "©\uFE0F"; // U+00A9 + U+FE0F (emoji style)
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/a9.png\" alt=\"©\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_SurrogatePairTest()
- {
- var origText = "🍣"; // U+1F363
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f363.png\" alt=\"🍣\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_CombiningCharacterTest()
- {
- var origText = "#⃣"; // U+0023 U+20E3 (合字)
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/23-20e3.png\" alt=\"#⃣\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_Unicode8Test()
- {
- // Unicode 8.0 で追加された絵文字
- var origText = "🌭"; // U+1F32D (HOT DOG)
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f32d.png\" alt=\"🌭\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_Emoji50Test()
- {
- // Unicode 10.0/Emoji 5.0 で追加された絵文字
- var origText = "🦒"; // U+1F992 (GIRAFFE)
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f992.png\" alt=\"🦒\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_EmojiModifiers_CombiningTest()
- {
- // Emoji modifiers を使用した合字 (リガチャー)
- var origText = "👦\U0001F3FF"; // U+1F466 (BOY) + U+1F3FF (EMOJI MODIFIER FITZPATRICK TYPE-6)
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f466-1f3ff.png\" alt=\"👦\U0001F3FF\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_EmojiModifiers_SingleTest()
- {
- // Emoji modifiers は単体でも絵文字として表示される
- var origText = "\U0001F3FF"; // U+1F3FB (EMOJI MODIFIER FITZPATRICK TYPE-6)
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f3ff.png\" alt=\"\U0001F3FF\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_EmojiZWJSequenceTest()
- {
- // 複数の絵文字を U+200D (ZERO WIDTH JOINER) で繋げて表現する絵文字
- var origText = "👨\u200D🎨"; // U+1F468 (MAN) + U+200D + U+1F3A8 (ARTIST PALETTE)
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f468-200d-1f3a8.png\" alt=\"👨\u200D🎨\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_EmojiZWJSequenceWithVariationSelectorTest()
- {
- // 複数の絵文字を U+200D (ZERO WIDTH JOINER) で繋げて表現 + 異字体セレクタ U+FE0F を含む絵文字
- // この場合は URL 生成時に異字体セレクタ U+FE0F を除去しない
- var origText = "🏃\u200D♀\uFE0F"; // U+1F3C3 (RUNNER) + U+200D + U+2640 (FEMARE SIGN) + U+FE0F
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f3c3-200d-2640-fe0f.png\" alt=\"🏃\u200D♀\uFE0F\" />";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_NotEmojiTest()
- {
- var origText = "123ABC";
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "123ABC";
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void ReplaceEmojiToImg_HtmlTest()
- {
- // 属性内の絵文字は変換しない
- var origText = "🐟<a href='http://xn--7c9bw4k.jp/' title='🍣.jp'>🍣.jp</a>🐡";
-
- var result = EmojiFormatter.ReplaceEmojiToImg(origText);
- var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f41f.png\" alt=\"🐟\" />" +
- "<a href='http://xn--7c9bw4k.jp/' title='🍣.jp'>" +
- "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f363.png\" alt=\"🍣\" />.jp" +
- "</a>" +
- "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f421.png\" alt=\"🐡\" />";
-
- Assert.Equal(expected, result);
- }
- }
-}
Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities));
}
+
+ [Fact]
+ public void FormatEmojiEntity_Test()
+ {
+ var text = "🍣";
+ var entities = new[]
+ {
+ new TwitterEntityEmoji
+ {
+ Indices = new[] { 0, 1 },
+ Text = "🍣",
+ Url = "https://twemoji.maxcdn.com/2/72x72/1f363.png",
+ },
+ };
+
+ var expected = "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/1f363.png\" alt=\"🍣\" />";
+ Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities));
+ }
[Fact]
public void AutoLinkHtml_EntityNullTest()
{
+++ /dev/null
-// OpenTween - Client of Twitter
-// Copyright (c) 2015 takke (@takke) <http://takke.jp/>
-// All rights reserved.
-//
-// This file is part of OpenTween.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-// for more details.
-//
-// You should have received a copy of the GNU General Public License along
-// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
-// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
-// Boston, MA 02110-1301, USA.
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-
-namespace OpenTween
-{
- /// <summary>
- /// ツイート内の絵文字を img タグに書き換えるクラス
- /// </summary>
- public static class EmojiFormatter
- {
- public static string ReplaceEmojiToImg(string text)
- {
- // HTMLタグを壊さないように <...> の外側のみを置換する
- return Regex.Replace(text, @"^[^<]+$|^[^<]+(?:<)|(?:>)[^<]+(?:<)|(?:>)[^<]+$", m =>
- {
- return TweetExtractor.EmojiPattern.Replace(m.Value, ReplaceEmojiEntity);
- });
- }
-
- private static string ReplaceEmojiEntity(System.Text.RegularExpressions.Match m)
- {
- string input = m.Value;
- string codes = "";
-
- // 異字体セレクタ U+FE0F (emoji style) は除去する (ZWJ を含まない場合のみ)
- if (!input.Contains('\u200D'))
- input = input.Replace("\uFE0F", "");
-
- for (var i = 0; i < input.Length; i += char.IsSurrogatePair(input, i) ? 2 : 1)
- {
- var codepoint = char.ConvertToUtf32(input, i);
-
- if (i > 0)
- {
- codes += "-";
- }
-
- codes += string.Format("{0:x}", codepoint);
- }
-
- return "<img class=\"emoji\" src=\"https://twemoji.maxcdn.com/2/72x72/" + codes + ".png\" alt=\"" + input + "\" />";
- }
- }
-}
<Compile Include="Connection\OAuthUtility.cs" />
<Compile Include="Connection\TwitterApiConnection.cs" />
<Compile Include="DateTimeUtc.cs" />
- <Compile Include="EmojiFormatter.cs" />
<Compile Include="EventViewerDialog.cs">
<SubType>Form</SubType>
</Compile>
}
public string createDetailHtml(string orgdata)
- {
- if (SettingManager.Local.UseTwemoji)
- orgdata = EmojiFormatter.ReplaceEmojiToImg(orgdata);
-
- return detailHtmlFormatHeader + orgdata + detailHtmlFormatFooter;
- }
+ => detailHtmlFormatHeader + orgdata + detailHtmlFormatFooter;
private Task DispSelectedPost()
=> this.DispSelectedPost(false);
using System.Text;
using System.Text.RegularExpressions;
using OpenTween.Api.DataModel;
+using OpenTween.Setting;
namespace OpenTween
{
yield return FormatHashtagEntity(targetText, hashtagEntity);
else if (entity is TwitterEntityMention mentionEntity)
yield return FormatMentionEntity(targetText, mentionEntity);
+ else if (entity is TwitterEntityEmoji emojiEntity)
+ yield return FormatEmojiEntity(targetText, emojiEntity);
else
yield return t(e(targetText));
private static string FormatMentionEntity(string targetText, TwitterEntityMention entity)
=> "<a class=\"mention\" href=\"https://twitter.com/" + eu(entity.ScreenName) + "\">" + t(e(targetText)) + "</a>";
+ private static string FormatEmojiEntity(string targetText, TwitterEntityEmoji entity)
+ {
+ if (!SettingManager.Local.UseTwemoji)
+ return t(e(targetText));
+
+ return "<img class=\"emoji\" src=\"" + e(entity.Url) + "\" alt=\"" + e(entity.Text) + "\" />";
+ }
+
// 長いのでエイリアスとして e(...), eu(...), t(...) でエスケープできるようにする
private static Func<string, string> e = EscapeHtml;
private static Func<string, string> eu = Uri.EscapeDataString;
internal static string CreateHtmlAnchor(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
{
+ var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(text));
+
// PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
- text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
+ text = TweetFormatter.AutoLinkHtml(text, mergedEntities, keepTco: true);
text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"http://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
text = PreProcessUrl(text); //IDN置換
entity.ExpandedUrl = await ShortUrl.Instance.ExpandUrlAsync(entity.ExpandedUrl);
// user.entities には urls 以外のエンティティが含まれていないため、テキストをもとに生成する
- entities.Hashtags = TweetExtractor.ExtractHashtagEntities(descriptionText).ToArray();
- entities.UserMentions = TweetExtractor.ExtractMentionEntities(descriptionText).ToArray();
+ var mergedEntities = entities.Urls.AsEnumerable<TwitterEntity>()
+ .Concat(TweetExtractor.ExtractHashtagEntities(descriptionText))
+ .Concat(TweetExtractor.ExtractMentionEntities(descriptionText))
+ .Concat(TweetExtractor.ExtractEmojiEntities(descriptionText));
- var html = TweetFormatter.AutoLinkHtml(descriptionText, entities);
+ var html = TweetFormatter.AutoLinkHtml(descriptionText, mergedEntities);
html = this.mainForm.createDetailHtml(html);
if (cancellationToken.IsCancellationRequested)
foreach (var entity in entities.Urls)
entity.ExpandedUrl = await ShortUrl.Instance.ExpandUrlAsync(entity.ExpandedUrl);
- var html = TweetFormatter.AutoLinkHtml(status.FullText, entities);
+ var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(status.FullText));
+
+ var html = TweetFormatter.AutoLinkHtml(status.FullText, mergedEntities);
html = this.mainForm.createDetailHtml(html +
" Posted at " + MyCommon.DateTimeParse(status.CreatedAt).ToLocalTimeString() +
" via " + status.Source);