OSDN Git Service

PostClass.Mediaを1つのURLごとに複数の画像URLを保持できるように修正
[opentween/open-tween.git] / OpenTween / TweetFormatter.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details.
16 //
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 using System;
23 using System.Collections.Generic;
24 using System.Globalization;
25 using System.Linq;
26 using System.Text;
27 using OpenTween.Api;
28
29 namespace OpenTween
30 {
31     /// <summary>
32     /// ツイートの Entity 情報をもとにリンク化などを施すクラス
33     /// </summary>
34     public class TweetFormatter
35     {
36         public static string AutoLinkHtml(string text, TwitterEntities entities)
37         {
38             if (entities == null)
39                 return AutoLinkHtml(text, Enumerable.Empty<TwitterEntity>());
40
41             var urls = entities.Urls ?? new TwitterEntityUrl[0];
42             var hashtags = entities.Hashtags ?? new TwitterEntityHashtag[0];
43             var mentions = entities.UserMentions ?? new TwitterEntityMention[0];
44             var media = entities.Media ?? new TwitterEntityMedia[0];
45
46             var entitiesQuery = urls.Cast<TwitterEntity>()
47                 .Concat(hashtags)
48                 .Concat(mentions)
49                 .Concat(media)
50                 .Where(x => x != null)
51                 .Where(x => x.Indices != null && x.Indices.Length == 2);
52
53             return string.Concat(AutoLinkHtmlInternal(text, entitiesQuery));
54         }
55
56         public static string AutoLinkHtml(string text, IEnumerable<TwitterEntity> entities)
57         {
58             if (entities == null)
59                 entities = Enumerable.Empty<TwitterEntity>();
60
61             var entitiesQuery = entities
62                 .Where(x => x != null)
63                 .Where(x => x.Indices != null && x.Indices.Length == 2);
64
65             return string.Concat(AutoLinkHtmlInternal(text, entitiesQuery));
66         }
67
68         private static IEnumerable<string> AutoLinkHtmlInternal(string text, IEnumerable<TwitterEntity> entities)
69         {
70             var curIndex = 0;
71
72             foreach (var entity in FixEntityIndices(text, entities))
73             {
74                 var startIndex = entity.Indices[0];
75                 var endIndex = entity.Indices[1];
76
77                 if (curIndex > startIndex)
78                     continue; // 区間が重複する不正なエンティティを無視する
79
80                 if (startIndex > endIndex)
81                     continue; // 区間が不正なエンティティを無視する
82
83                 if (startIndex > text.Length || endIndex > text.Length)
84                     continue; // 区間が文字列長を越えている不正なエンティティを無視する
85
86                 if (curIndex != startIndex)
87                     yield return t(e(text.Substring(curIndex, startIndex - curIndex)));
88
89                 var targetText = text.Substring(startIndex, endIndex - startIndex);
90
91                 if (entity is TwitterEntityUrl)
92                     yield return FormatUrlEntity(targetText, (TwitterEntityUrl)entity);
93                 else if (entity is TwitterEntityHashtag)
94                     yield return FormatHashtagEntity(targetText, (TwitterEntityHashtag)entity);
95                 else if (entity is TwitterEntityMention)
96                     yield return FormatMentionEntity(targetText, (TwitterEntityMention)entity);
97                 else
98                     yield return t(e(targetText));
99
100                 curIndex = endIndex;
101             }
102
103             if (curIndex != text.Length)
104                 yield return t(e(text.Substring(curIndex)));
105         }
106
107         /// <summary>
108         /// エンティティの Indices をサロゲートペアを考慮して調整します
109         /// </summary>
110         private static IEnumerable<TwitterEntity> FixEntityIndices(string text, IEnumerable<TwitterEntity> entities)
111         {
112             var curIndex = 0;
113             var indexOffset = 0; // サロゲートペアによる indices のズレを表す
114
115             foreach (var entity in entities.OrderBy(x => x.Indices[0]))
116             {
117                 var startIndex = entity.Indices[0];
118                 var endIndex = entity.Indices[1];
119
120                 for (var i = curIndex; i < (startIndex + indexOffset); i++)
121                     if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1]))
122                         indexOffset++;
123
124                 startIndex += indexOffset;
125                 curIndex = startIndex;
126
127                 for (var i = curIndex; i < (endIndex + indexOffset); i++)
128                     if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1]))
129                         indexOffset++;
130
131                 endIndex += indexOffset;
132                 curIndex = endIndex;
133
134                 entity.Indices[0] = startIndex;
135                 entity.Indices[1] = endIndex;
136
137                 yield return entity;
138             }
139         }
140
141         private static string FormatUrlEntity(string targetText, TwitterEntityUrl entity)
142         {
143             string expandedUrl;
144
145             // 過去に存在した壊れたエンティティの対策
146             // 参照: https://dev.twitter.com/discussions/12628
147             if (entity.DisplayUrl == null)
148             {
149                 expandedUrl = MyCommon.ConvertToReadableUrl(targetText);
150                 return "<a href=\"" + e(entity.Url) + "\" title=\"" + e(expandedUrl) + "\">" + t(e(targetText)) + "</a>";
151             }
152
153             expandedUrl = MyCommon.ConvertToReadableUrl(entity.ExpandedUrl);
154             return "<a href=\"" + e(entity.Url) + "\" title=\"" + e(expandedUrl) + "\">" + t(e(entity.DisplayUrl)) + "</a>";
155         }
156
157         private static string FormatHashtagEntity(string targetText, TwitterEntityHashtag entity)
158         {
159             return "<a class=\"hashtag\" href=\"https://twitter.com/search?q=%23" + eu(entity.Text) + "\">" + t(e(targetText)) + "</a>";
160         }
161
162         private static string FormatMentionEntity(string targetText, TwitterEntityMention entity)
163         {
164             return "<a class=\"mention\" href=\"https://twitter.com/" + eu(entity.ScreenName) + "\">" + t(e(targetText)) + "</a>";
165         }
166
167         // 長いのでエイリアスとして e(...), eu(...), t(...) でエスケープできるようにする
168         private static Func<string, string> e = EscapeHtml;
169         private static Func<string, string> eu = Uri.EscapeDataString;
170         private static Func<string, string> t = FilterText;
171
172         private static string EscapeHtml(string text)
173         {
174             // Twitter API は "<" ">" "&" だけ中途半端にエスケープした状態のテキストを返すため、
175             // これらの文字だけ一旦エスケープを解除する
176             text = text.Replace("&lt;", "<").Replace("&gt;", ">").Replace("&amp;", "&");
177
178             var result = new StringBuilder(100);
179             foreach (var c in text)
180             {
181                 // 「<」「>」「&」「"」「'」についてエスケープ処理を施す
182                 // 参照: http://d.hatena.ne.jp/ockeghem/20070510/1178813849
183                 switch (c)
184                 {
185                     case '<':
186                         result.Append("&lt;");
187                         break;
188                     case '>':
189                         result.Append("&gt;");
190                         break;
191                     case '&':
192                         result.Append("&amp;");
193                         break;
194                     case '"':
195                         result.Append("&quot;");
196                         break;
197                     case '\'':
198                         result.Append("&#39;");
199                         break;
200                     default:
201                         result.Append(c);
202                         break;
203                 }
204             }
205
206             return result.ToString();
207         }
208
209         /// <summary>
210         /// HTML の属性値ではない、通常のテキストに対するフィルタ処理
211         /// </summary>
212         private static string FilterText(string text)
213         {
214             text = text.Replace("\n", "<br>");
215
216             return text;
217         }
218     }
219 }