From d876ec32fa0138cd9f6427c3ac2284f38b1f910c Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Tue, 11 Feb 2014 14:44:11 +0900 Subject: [PATCH] =?utf8?q?Tweet=20Entity=E3=82=92=E4=BD=BF=E7=94=A8?= =?utf8?q?=E3=81=97=E3=81=9F=E3=83=AA=E3=83=B3=E3=82=AF=E5=8C=96=E5=87=A6?= =?utf8?q?=E7=90=86=E3=82=92=E6=9B=B8=E3=81=8D=E7=9B=B4=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 参照: https://sourceforge.jp/ticket/browse.php?group_id=6526&tid=33079 --- OpenTween.Tests/OpenTween.Tests.csproj | 1 + OpenTween.Tests/TweetFormatterTest.cs | 324 +++++++++++++++++++++++++++++++++ OpenTween/DataModel.cs | 21 +-- OpenTween/OpenTween.csproj | 1 + OpenTween/Resources/ChangeLog.txt | 1 + OpenTween/ShowUserInfo.cs | 2 +- OpenTween/TweetFormatter.cs | 214 ++++++++++++++++++++++ OpenTween/Twitter.cs | 130 ++----------- 8 files changed, 571 insertions(+), 123 deletions(-) create mode 100644 OpenTween.Tests/TweetFormatterTest.cs create mode 100644 OpenTween/TweetFormatter.cs diff --git a/OpenTween.Tests/OpenTween.Tests.csproj b/OpenTween.Tests/OpenTween.Tests.csproj index 4e8cbc78..a7e038c5 100644 --- a/OpenTween.Tests/OpenTween.Tests.csproj +++ b/OpenTween.Tests/OpenTween.Tests.csproj @@ -75,6 +75,7 @@ + diff --git a/OpenTween.Tests/TweetFormatterTest.cs b/OpenTween.Tests/TweetFormatterTest.cs new file mode 100644 index 00000000..4ba79993 --- /dev/null +++ b/OpenTween.Tests/TweetFormatterTest.cs @@ -0,0 +1,324 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2014 kim_upsilon (@kim_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 , 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 Xunit; +using Xunit.Extensions; + +namespace OpenTween +{ + public class TweetFormatterTest + { + [Fact] + public void FormatUrlEntity_Test() + { + var text = "http://t.co/KYi7vMZzRt"; + var entities = new[] + { + new TwitterDataModel.Urls + { + Indices = new[] { 0, 22 }, + DisplayUrl = "twitter.com", + ExpandedUrl = "http://twitter.com/", + Url = "http://t.co/KYi7vMZzRt", + }, + }; + + var expected = "twitter.com"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void FormatHashtagEntity_Test() + { + var text = "#OpenTween"; + var entities = new[] + { + new TwitterDataModel.Hashtags + { + Indices = new[] { 0, 10 }, + Text = "OpenTween", + }, + }; + + var expected = "#OpenTween"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void FormatMentionEntity_Test() + { + var text = "@TwitterAPI"; + var entities = new[] + { + new TwitterDataModel.UserMentions + { + Indices = new[] { 0, 11 }, + Id = 6253282L, + Name = "Twitter API", + ScreenName = "twitterapi", + }, + }; + + var expected = "@TwitterAPI"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void FormatMediaEntity_Test() + { + var text = "http://t.co/h5dCr4ftN4"; + var entities = new[] + { + new TwitterDataModel.Media + { + Indices = new[] { 0, 22 }, + Sizes = new TwitterDataModel.Sizes + { + Large = new TwitterDataModel.SizeElement { Resize = "fit", h = 329, w = 1024 }, + Medium = new TwitterDataModel.SizeElement { Resize = "fit", h = 204, w = 600 }, + Small = new TwitterDataModel.SizeElement { Resize = "fit", h = 116, w = 340 }, + Thumb = new TwitterDataModel.SizeElement { Resize = "crop", h = 150, w = 150 }, + }, + Type = "photo", + Id = 426404550379986940L, + MediaUrl = "http://pbs.twimg.com/media/BerkrewCYAAV4Kf.png", + MediaUrlHttps = "https://pbs.twimg.com/media/BerkrewCYAAV4Kf.png", + Url = "http://t.co/h5dCr4ftN4", + DisplayUrl = "pic.twitter.com/h5dCr4ftN4", + ExpandedUrl = "http://twitter.com/kim_upsilon/status/426404550371598337/photo/1", + }, + }; + + var expected = "pic.twitter.com/h5dCr4ftN4"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_EntityNullTest() + { + var text = "てすとてすとー"; + TwitterDataModel.Entities entities = null; + + var expected = "てすとてすとー"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_EntityNullTest2() + { + var text = "てすとてすとー"; + TwitterDataModel.Entities entities = new TwitterDataModel.Entities + { + Urls = null, + Hashtags = null, + UserMentions = null, + Media = null, + }; + + var expected = "てすとてすとー"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_EntityNullTest3() + { + var text = "てすとてすとー"; + IEnumerable entities = null; + + var expected = "てすとてすとー"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_EntityNullTest4() + { + var text = "てすとてすとー"; + IEnumerable entities = new TwitterDataModel.Entity[] { null }; + + var expected = "てすとてすとー"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_EscapeTest() + { + // Twitter APIの中途半端なエスケープの対象とならない「"」や「'」に対するエスケープ処理を施す + var text = "\"\'@twitterapi\'\""; + var entities = new[] + { + new TwitterDataModel.UserMentions + { + Indices = new[] { 2, 13 }, + Id = 6253282L, + Name = "Twitter API", + ScreenName = "twitterapi", + }, + }; + + var expected = ""'@twitterapi'""; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_EscapeTest2() + { + // 「<」や「>」についてはエスケープされた状態でAPIからテキストが返されるため、二重エスケープとならないように考慮する + var text = "<b> @twitterapi </b>"; + var entities = new[] + { + new TwitterDataModel.UserMentions + { + Indices = new[] { 10, 21 }, + Id = 6253282L, + Name = "Twitter API", + ScreenName = "twitterapi", + }, + }; + + var expected = "<b> @twitterapi </b>"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_EscapeTest3() + { + // 万が一「<」や「>」がエスケープされていない状態のテキストを受け取っても適切にエスケープが施されるようにする + var text = " @twitterapi "; + var entities = new[] + { + new TwitterDataModel.UserMentions + { + Indices = new[] { 4, 15 }, + Id = 6253282L, + Name = "Twitter API", + ScreenName = "twitterapi", + }, + }; + + var expected = "<b> @twitterapi </b>"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_EscapeUrlTest() + { + // 日本語ハッシュタグのリンク先URLを適切にエスケープする + var text = "#ぜんぶ雪のせいだ"; + var entities = new[] + { + new TwitterDataModel.Hashtags + { + Indices = new[] { 0, 9 }, + Text = "ぜんぶ雪のせいだ", + }, + }; + + var expected = "#ぜんぶ雪のせいだ"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_SurrogatePairTest() + { + // UTF-16 で 4 バイトで表される文字を含むツイート + // 参照: https://sourceforge.jp/ticket/browse.php?group_id=6526&tid=33079 + var text = "🐬🐬 @irucame 🐬🐬"; + var entities = new[] + { + new TwitterDataModel.UserMentions + { + Indices = new[] { 3, 11 }, + Id = 89942943L, + ScreenName = "irucame", + }, + }; + + var expected = "🐬🐬 @irucame 🐬🐬"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_SurrogatePairTest2() + { + // 現時点では存在しないものの、ハッシュタグなどエンティティ内にサロゲートペアが含まれる場合も考慮する + var text = "🐬🐬 #🐬🐬 🐬🐬 #🐬🐬 🐬🐬"; + var entities = new[] + { + new TwitterDataModel.Hashtags + { + Indices = new[] { 3, 6 }, + Text = "🐬🐬", + }, + new TwitterDataModel.Hashtags + { + Indices = new[] { 10, 13 }, + Text = "🐬🐬", + }, + }; + + var expected = "🐬🐬 #🐬🐬 " + + "🐬🐬 #🐬🐬 🐬🐬"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_CompositeCharacterTest() + { + // 合成文字 é ( \u00e9 ) を含むツイート + // 参照: https://dev.twitter.com/issues/251 + var text = "Caf\u00e9 #test"; + var entities = new[] + { + new TwitterDataModel.Hashtags + { + Indices = new[] { 5, 10 }, + Text = "test", + }, + }; + + var expected = "Caf\u00e9 #test"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + + [Fact] + public void AutoLinkHtml_CombiningCharacterSequenceTest() + { + // 結合文字列 é ( e + \u0301 ) を含むツイート + // 参照: https://dev.twitter.com/issues/251 + var text = "Cafe\u0301 #test"; + var entities = new[] + { + new TwitterDataModel.Hashtags + { + Indices = new[] { 6, 11 }, + Text = "test", + }, + }; + + var expected = "Cafe\u0301 #test"; + Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); + } + } +} diff --git a/OpenTween/DataModel.cs b/OpenTween/DataModel.cs index d4f73948..b25468c3 100644 --- a/OpenTween/DataModel.cs +++ b/OpenTween/DataModel.cs @@ -41,6 +41,12 @@ namespace OpenTween } [DataContract] + public class Entity + { + [DataMember(Name = "indices")] public int[] Indices = new int[3]; + } + + [DataContract] public class SizeElement { [DataMember(Name = "w")] public int w; @@ -58,39 +64,32 @@ namespace OpenTween } [DataContract] - public class Media + public class Media : Urls { [DataMember(Name = "id")] public long Id; [DataMember(Name = "media_url")] public string MediaUrl; [DataMember(Name = "media_url_https")] public string MediaUrlHttps; - [DataMember(Name = "url")] public string Url; - [DataMember(Name = "display_url")] public string DisplayUrl; - [DataMember(Name = "expanded_url")] public string ExpandedUrl; [DataMember(Name = "sizes")] public Sizes Sizes; [DataMember(Name = "type")] public string Type; - [DataMember(Name = "indices")] public int[] Indices = new int[3]; } [DataContract] - public class Urls + public class Urls : Entity { [DataMember(Name = "url")] public string Url; [DataMember(Name = "display_url")] public string DisplayUrl; [DataMember(Name = "expanded_url")] public string ExpandedUrl; - [DataMember(Name = "indices")] public int[] Indices = new int[3]; } [DataContract] - public class Hashtags + public class Hashtags : Entity { - [DataMember(Name = "indices")] public int[] Indices = new int[3]; [DataMember(Name = "text")] public string Text; } [DataContract] - public class UserMentions + public class UserMentions : Entity { - [DataMember(Name = "indices")] public int[] Indices = new int[3]; [DataMember(Name = "screen_name")] public string ScreenName; [DataMember(Name = "name")] public string Name; [DataMember(Name = "id")] public Int64 Id; diff --git a/OpenTween/OpenTween.csproj b/OpenTween/OpenTween.csproj index e0d7dbf3..9952ad4a 100644 --- a/OpenTween/OpenTween.csproj +++ b/OpenTween/OpenTween.csproj @@ -270,6 +270,7 @@ + UserControl diff --git a/OpenTween/Resources/ChangeLog.txt b/OpenTween/Resources/ChangeLog.txt index 2fedb891..a6ba66ea 100644 --- a/OpenTween/Resources/ChangeLog.txt +++ b/OpenTween/Resources/ChangeLog.txt @@ -8,6 +8,7 @@ * FIX: サムネイル画像の読み込み中に画面の更新が遅くなる問題の修正 * FIX: 壊れたプロフィール画像を取得したり不安定な回線で読み込みを行うと操作不能な状態になることがある問題の修正 * FIX: Twitter API から取得したツイートのエンティティ情報が不正な場合の対策を追加 (thx @nyamph_pf!) + * FIX: サロゲートペアを含むツイートのリンク化処理が正しく行われない問題の修正 ==== Ver 1.1.7(2014/01/16) * NEW: ダイレクトメッセージに添付された画像のサムネイル表示に対応 diff --git a/OpenTween/ShowUserInfo.cs b/OpenTween/ShowUserInfo.cs index a41723a8..5c710cb1 100644 --- a/OpenTween/ShowUserInfo.cs +++ b/OpenTween/ShowUserInfo.cs @@ -180,7 +180,7 @@ namespace OpenTween if (_info.RecentPost != null) { recentPostTxt = MyOwner.createDetailHtml( - MyOwner.TwitterInstance.CreateHtmlAnchor(ref _info.RecentPost, atlist, userInfo.Status.Entities, null) + + MyOwner.TwitterInstance.CreateHtmlAnchor(_info.RecentPost, atlist, userInfo.Status.Entities, null) + " Posted at " + _info.PostCreatedAt.ToString() + " via " + _info.PostSource); } diff --git a/OpenTween/TweetFormatter.cs b/OpenTween/TweetFormatter.cs new file mode 100644 index 00000000..a1b786d8 --- /dev/null +++ b/OpenTween/TweetFormatter.cs @@ -0,0 +1,214 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2014 kim_upsilon (@kim_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 , 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.Globalization; +using System.Linq; +using System.Text; + +namespace OpenTween +{ + /// + /// ツイートの Entity 情報をもとにリンク化などを施すクラス + /// + public class TweetFormatter + { + public static string AutoLinkHtml(string text, TwitterDataModel.Entities entities) + { + if (entities == null) + return AutoLinkHtml(text, Enumerable.Empty()); + + if (entities.Urls == null) + entities.Urls = new TwitterDataModel.Urls[0]; + + if (entities.Hashtags == null) + entities.Hashtags = new TwitterDataModel.Hashtags[0]; + + if (entities.UserMentions == null) + entities.UserMentions = new TwitterDataModel.UserMentions[0]; + + if (entities.Media == null) + entities.Media = new TwitterDataModel.Media[0]; + + var entitiesQuery = entities.Urls.Cast() + .Concat(entities.Hashtags) + .Concat(entities.UserMentions) + .Concat(entities.Media) + .Where(x => x != null) + .Where(x => x.Indices != null && x.Indices.Length == 2); + + return string.Concat(AutoLinkHtmlInternal(text, entitiesQuery)); + } + + public static string AutoLinkHtml(string text, IEnumerable entities) + { + if (entities == null) + entities = Enumerable.Empty(); + + var entitiesQuery = entities + .Where(x => x != null) + .Where(x => x.Indices != null && x.Indices.Length == 2); + + return string.Concat(AutoLinkHtmlInternal(text, entitiesQuery)); + } + + private static IEnumerable AutoLinkHtmlInternal(string text, IEnumerable entities) + { + var curIndex = 0; + + foreach (var entity in FixEntityIndices(text, entities)) + { + var startIndex = entity.Indices[0]; + var endIndex = entity.Indices[1]; + + if (curIndex > startIndex) + continue; // 区間が重複する不正なエンティティを無視する + + if (startIndex > endIndex) + continue; // 区間が不正なエンティティを無視する + + if (startIndex > text.Length || endIndex > text.Length) + continue; // 区間が文字列長を越えている不正なエンティティを無視する + + if (curIndex != startIndex) + yield return e(text.Substring(curIndex, startIndex - curIndex)); + + var targetText = text.Substring(startIndex, endIndex - startIndex); + + if (entity is TwitterDataModel.Urls) + yield return FormatUrlEntity(targetText, (TwitterDataModel.Urls)entity); + else if (entity is TwitterDataModel.Hashtags) + yield return FormatHashtagEntity(targetText, (TwitterDataModel.Hashtags)entity); + else if (entity is TwitterDataModel.UserMentions) + yield return FormatMentionEntity(targetText, (TwitterDataModel.UserMentions)entity); + else + yield return e(targetText); + + curIndex = endIndex; + } + + if (curIndex != text.Length) + yield return e(text.Substring(curIndex)); + } + + /// + /// エンティティの Indices をサロゲートペアを考慮して調整します + /// + private static IEnumerable FixEntityIndices(string text, IEnumerable entities) + { + var curIndex = 0; + var indexOffset = 0; // サロゲートペアによる indices のズレを表す + + foreach (var entity in entities.OrderBy(x => x.Indices[0])) + { + var startIndex = entity.Indices[0]; + var endIndex = entity.Indices[1]; + + for (var i = curIndex; i < (startIndex + indexOffset); i++) + if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1])) + indexOffset++; + + startIndex += indexOffset; + curIndex = startIndex; + + for (var i = curIndex; i < (endIndex + indexOffset); i++) + if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1])) + indexOffset++; + + endIndex += indexOffset; + curIndex = endIndex; + + entity.Indices[0] = startIndex; + entity.Indices[1] = endIndex; + + yield return entity; + } + } + + private static string FormatUrlEntity(string targetText, TwitterDataModel.Urls entity) + { + string expandedUrl; + + // 過去に存在した壊れたエンティティの対策 + // 参照: https://dev.twitter.com/discussions/12628 + if (entity.DisplayUrl == null) + { + expandedUrl = MyCommon.ConvertToReadableUrl(targetText); + return "" + e(targetText) + ""; + } + + expandedUrl = MyCommon.ConvertToReadableUrl(entity.ExpandedUrl); + return "" + e(entity.DisplayUrl) + ""; + } + + private static string FormatHashtagEntity(string targetText, TwitterDataModel.Hashtags entity) + { + return "" + e(targetText) + ""; + } + + private static string FormatMentionEntity(string targetText, TwitterDataModel.UserMentions entity) + { + return "" + e(targetText) + ""; + } + + // 長いのでエイリアスとして e(...), eu(...) でエスケープできるようにする + private static Func e = EscapeHtml; + private static Func eu = Uri.EscapeDataString; + + private static string EscapeHtml(string text) + { + // Twitter API は "<" と ">" だけ中途半端にエスケープした状態のテキストを返すため、 + // これらの文字だけ一旦エスケープを解除する + text = text.Replace("<", "<").Replace(">", ">"); + + var result = new StringBuilder(100); + foreach (var c in text) + { + // 「<」「>」「&」「"」「'」についてエスケープ処理を施す + // 参照: http://d.hatena.ne.jp/ockeghem/20070510/1178813849 + switch (c) + { + case '<': + result.Append("<"); + break; + case '>': + result.Append(">"); + break; + case '&': + result.Append("&"); + break; + case '"': + result.Append("""); + break; + case '\'': + result.Append("'"); + break; + default: + result.Append(c); + break; + } + } + + return result.ToString(); + } + } +} diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index b54b8b07..f8d2c5b9 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -1615,7 +1615,7 @@ namespace OpenTween } //HTMLに整形 string textFromApi = post.TextFromApi; - post.Text = CreateHtmlAnchor(ref textFromApi, post.ReplyToList, entities, post.Media); + post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media); post.TextFromApi = textFromApi; post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities); post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); @@ -2028,7 +2028,7 @@ namespace OpenTween //本文 var textFromApi = message.Text; //HTMLに整形 - post.Text = CreateHtmlAnchor(ref textFromApi, post.ReplyToList, message.Entities, post.Media); + post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media); post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities); post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); @@ -2271,7 +2271,7 @@ namespace OpenTween } //HTMLに整形 string textFromApi = post.TextFromApi; - post.Text = CreateHtmlAnchor(ref textFromApi, post.ReplyToList, entities, post.Media); + post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media); post.TextFromApi = textFromApi; post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities); post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); @@ -2922,144 +2922,52 @@ namespace OpenTween return retStr; } - private class EntityInfo + public string CreateHtmlAnchor(string text, List AtList, TwitterDataModel.Entities entities, Dictionary media) { - public int StartIndex { get; set; } - public int EndIndex { get; set; } - public string Text { get; set; } - public string Html { get; set; } - public string Display { get; set; } - } - public string CreateHtmlAnchor(ref string Text, List AtList, TwitterDataModel.Entities entities, Dictionary media) - { - var ret = Text; - if (entities != null) { - var etInfo = new SortedList(); - //URL if (entities.Urls != null) { foreach (var ent in entities.Urls) { - var startIndex = ent.Indices[0]; - var endIndex = ent.Indices[1]; - - if (etInfo.ContainsKey(startIndex)) - continue; + ent.ExpandedUrl = ShortUrl.ResolveMedia(ent.ExpandedUrl, false); - if (string.IsNullOrEmpty(ent.DisplayUrl)) - { - etInfo.Add(startIndex, - new EntityInfo {StartIndex = startIndex, - EndIndex = endIndex, - Text = ent.Url, - Html = "" + ent.Url + ""}); - } - else - { - var expanded = ShortUrl.ResolveMedia(ent.ExpandedUrl, false); - etInfo.Add(startIndex, - new EntityInfo {StartIndex = startIndex, - EndIndex = endIndex, - Text = ent.Url, - Html = "" + ent.DisplayUrl + "", - Display = ent.DisplayUrl}); - if (media != null && !media.ContainsKey(ent.Url)) media.Add(ent.Url, expanded); - } + if (media != null && !media.ContainsKey(ent.Url)) + media.Add(ent.Url, ent.ExpandedUrl); } } if (entities.Hashtags != null) { - foreach (var ent in entities.Hashtags) + lock (this.LockObj) { - var startIndex = ent.Indices[0]; - var endIndex = ent.Indices[1]; - - if (etInfo.ContainsKey(startIndex)) - continue; - - var hash = Text.Substring(startIndex, endIndex - startIndex); - etInfo.Add(startIndex, - new EntityInfo {StartIndex = startIndex, - EndIndex = endIndex, - Text = hash, - Html = "" + hash + ""}); - lock (LockObj) - { - _hashList.Add("#" + ent.Text); - } + this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text)); } } if (entities.UserMentions != null) { foreach (var ent in entities.UserMentions) { - var startIndex = ent.Indices[0] + 1; - var endIndex = ent.Indices[1]; - - if (etInfo.ContainsKey(startIndex)) - continue; - - var screenName = Text.Substring(startIndex, endIndex - startIndex); - etInfo.Add(startIndex, - new EntityInfo {StartIndex = startIndex, - EndIndex = endIndex, - Text = ent.ScreenName, - Html = "" + screenName + ""}); - if (!AtList.Contains(ent.ScreenName.ToLower())) AtList.Add(ent.ScreenName.ToLower()); + var screenName = ent.ScreenName.ToLower(); + if (!AtList.Contains(screenName)) + AtList.Add(screenName); } } if (entities.Media != null) { foreach (var ent in entities.Media) { - if (ent.Type == "photo") - { - var startIndex = ent.Indices[0]; - var endIndex = ent.Indices[1]; - - // entities.Urls との重複を考慮 - if (etInfo.ContainsKey(startIndex)) - continue; - - etInfo.Add(startIndex, - new EntityInfo {StartIndex = startIndex, - EndIndex = endIndex, - Text = ent.Url, - Html = "" + ent.DisplayUrl + "", - Display = ent.DisplayUrl}); - if (media != null && !media.ContainsKey(ent.Url)) media.Add(ent.Url, ent.MediaUrl); - } - } - } - if (etInfo.Count > 0) - { - try - { - var idx = 0; - ret = ""; - foreach (var et in etInfo) - { - ret += Text.Substring(idx, et.Key - idx) + et.Value.Html; - idx = et.Value.EndIndex; - } - ret += Text.Substring(idx); - } - catch(ArgumentOutOfRangeException) - { - //Twitterのバグで不正なエンティティ(Index指定範囲が重なっている)が返ってくる場合の対応 - ret = Text; - entities = null; - if (media != null) media.Clear(); + if (media != null && !media.ContainsKey(ent.Url)) + media.Add(ent.Url, ent.MediaUrl); } } } - ret = Regex.Replace(ret, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1$2$3"); - ret = AdjustHtml(ShortUrl.Resolve(PreProcessUrl(ret), false)); //IDN置換、短縮Uri解決、@リンクを相対→絶対にしてtarget属性付与 + text = TweetFormatter.AutoLinkHtml(text, entities); - return ret; + text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1$2$3"); + text = AdjustHtml(ShortUrl.Resolve(PreProcessUrl(text), false)); //IDN置換、短縮Uri解決、@リンクを相対→絶対にしてtarget属性付与 + + return text; } //Source整形 -- 2.11.0