OSDN Git Service

PostClass.CreatedAtの型をDateTimeUtcに変更
[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 System.Text.RegularExpressions;
28 using OpenTween.Api.DataModel;
29
30 namespace OpenTween
31 {
32     /// <summary>
33     /// ツイートの Entity 情報をもとにリンク化などを施すクラス
34     /// </summary>
35     public static class TweetFormatter
36     {
37         public static string AutoLinkHtml(string text, IEnumerable<TwitterEntity> entities, bool keepTco = false)
38         {
39             if (entities == null)
40                 entities = Enumerable.Empty<TwitterEntity>();
41
42             var entitiesQuery = entities
43                 .Where(x => x != null)
44                 .Where(x => x.Indices != null && x.Indices.Length == 2);
45
46             return string.Concat(AutoLinkHtmlInternal(text, entitiesQuery, keepTco));
47         }
48
49         private static IEnumerable<string> AutoLinkHtmlInternal(string text, IEnumerable<TwitterEntity> entities, bool keepTco)
50         {
51             var curIndex = 0;
52
53             foreach (var entity in FixEntityIndices(text, entities))
54             {
55                 var startIndex = entity.Indices[0];
56                 var endIndex = entity.Indices[1];
57
58                 if (curIndex > startIndex)
59                     continue; // 区間が重複する不正なエンティティを無視する
60
61                 if (startIndex > endIndex)
62                     continue; // 区間が不正なエンティティを無視する
63
64                 if (startIndex > text.Length || endIndex > text.Length)
65                     continue; // 区間が文字列長を越えている不正なエンティティを無視する
66
67                 if (curIndex != startIndex)
68                     yield return t(e(text.Substring(curIndex, startIndex - curIndex)));
69
70                 var targetText = text.Substring(startIndex, endIndex - startIndex);
71
72                 if (entity is TwitterEntityUrl urlEntity)
73                     yield return FormatUrlEntity(targetText, urlEntity, keepTco);
74                 else if (entity is TwitterEntityHashtag hashtagEntity)
75                     yield return FormatHashtagEntity(targetText, hashtagEntity);
76                 else if (entity is TwitterEntityMention mentionEntity)
77                     yield return FormatMentionEntity(targetText, mentionEntity);
78                 else
79                     yield return t(e(targetText));
80
81                 curIndex = endIndex;
82             }
83
84             if (curIndex != text.Length)
85                 yield return t(e(text.Substring(curIndex)));
86         }
87
88         /// <summary>
89         /// エンティティの Indices をサロゲートペアを考慮して調整します
90         /// </summary>
91         private static IEnumerable<TwitterEntity> FixEntityIndices(string text, IEnumerable<TwitterEntity> entities)
92         {
93             var curIndex = 0;
94             var indexOffset = 0; // サロゲートペアによる indices のズレを表す
95
96             foreach (var entity in entities.OrderBy(x => x.Indices[0]))
97             {
98                 var startIndex = entity.Indices[0];
99                 var endIndex = entity.Indices[1];
100
101                 for (var i = curIndex; i < (startIndex + indexOffset); i++)
102                     if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1]))
103                         indexOffset++;
104
105                 startIndex += indexOffset;
106                 curIndex = startIndex;
107
108                 for (var i = curIndex; i < (endIndex + indexOffset); i++)
109                     if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1]))
110                         indexOffset++;
111
112                 endIndex += indexOffset;
113                 curIndex = endIndex;
114
115                 entity.Indices[0] = startIndex;
116                 entity.Indices[1] = endIndex;
117
118                 yield return entity;
119             }
120         }
121
122         private static string FormatUrlEntity(string targetText, TwitterEntityUrl entity, bool keepTco)
123         {
124             string expandedUrl;
125
126             // 過去に存在した壊れたエンティティの対策
127             // 参照: https://dev.twitter.com/discussions/12628
128             if (entity.DisplayUrl == null)
129             {
130                 expandedUrl = MyCommon.ConvertToReadableUrl(targetText);
131                 return "<a href=\"" + e(entity.Url) + "\" title=\"" + e(expandedUrl) + "\">" + t(e(targetText)) + "</a>";
132             }
133
134             var linkUrl = entity.Url;
135
136             expandedUrl = keepTco ? linkUrl : MyCommon.ConvertToReadableUrl(entity.ExpandedUrl);
137
138             var mediaEntity = entity as TwitterEntityMedia;
139
140             var titleText = mediaEntity?.AltText ?? expandedUrl;
141
142             // twitter.com へのリンクは t.co を経由せずに直接リンクする (但し pic.twitter.com はそのまま)
143             if (mediaEntity == null)
144             {
145                 if (entity.ExpandedUrl.StartsWith("https://twitter.com/", StringComparison.Ordinal) ||
146                     entity.ExpandedUrl.StartsWith("http://twitter.com/", StringComparison.Ordinal))
147                 {
148                     linkUrl = entity.ExpandedUrl;
149                 }
150             }
151
152             return "<a href=\"" + e(linkUrl) + "\" title=\"" + e(titleText) + "\">" + t(e(entity.DisplayUrl)) + "</a>";
153         }
154
155         private static string FormatHashtagEntity(string targetText, TwitterEntityHashtag entity)
156         {
157             return "<a class=\"hashtag\" href=\"https://twitter.com/search?q=%23" + eu(entity.Text) + "\">" + t(e(targetText)) + "</a>";
158         }
159
160         private static string FormatMentionEntity(string targetText, TwitterEntityMention entity)
161         {
162             return "<a class=\"mention\" href=\"https://twitter.com/" + eu(entity.ScreenName) + "\">" + t(e(targetText)) + "</a>";
163         }
164
165         // 長いのでエイリアスとして e(...), eu(...), t(...) でエスケープできるようにする
166         private static Func<string, string> e = EscapeHtml;
167         private static Func<string, string> eu = Uri.EscapeDataString;
168         private static Func<string, string> t = FilterText;
169
170         private static string EscapeHtml(string text)
171         {
172             // Twitter API は "<" ">" "&" だけ中途半端にエスケープした状態のテキストを返すため、
173             // これらの文字だけ一旦エスケープを解除する
174             text = text.Replace("&lt;", "<").Replace("&gt;", ">").Replace("&amp;", "&");
175
176             var result = new StringBuilder(100);
177             foreach (var c in text)
178             {
179                 // 「<」「>」「&」「"」「'」についてエスケープ処理を施す
180                 // 参照: http://d.hatena.ne.jp/ockeghem/20070510/1178813849
181                 switch (c)
182                 {
183                     case '<':
184                         result.Append("&lt;");
185                         break;
186                     case '>':
187                         result.Append("&gt;");
188                         break;
189                     case '&':
190                         result.Append("&amp;");
191                         break;
192                     case '"':
193                         result.Append("&quot;");
194                         break;
195                     case '\'':
196                         result.Append("&#39;");
197                         break;
198                     default:
199                         result.Append(c);
200                         break;
201                 }
202             }
203
204             return result.ToString();
205         }
206
207         /// <summary>
208         /// HTML の属性値ではない、通常のテキストに対するフィルタ処理
209         /// </summary>
210         private static string FilterText(string text)
211         {
212             text = text.Replace("\n", "<br>");
213             text = Regex.Replace(text, "  ", " &nbsp;");
214
215             return text;
216         }
217     }
218 }