1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
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)
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
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.
23 using System.Collections.Generic;
24 using System.Globalization;
27 using System.Text.RegularExpressions;
28 using OpenTween.Api.DataModel;
29 using OpenTween.Setting;
34 /// ツイートの Entity 情報をもとにリンク化などを施すクラス
36 public static class TweetFormatter
38 public static string AutoLinkHtml(string text, IEnumerable<TwitterEntity> entities, bool keepTco = false)
41 entities = Enumerable.Empty<TwitterEntity>();
43 var entitiesQuery = entities
44 .Where(x => x != null)
45 .Where(x => x.Indices != null && x.Indices.Length == 2);
47 return string.Concat(AutoLinkHtmlInternal(text, entitiesQuery, keepTco));
50 private static IEnumerable<string> AutoLinkHtmlInternal(string text, IEnumerable<TwitterEntity> entities, bool keepTco)
54 foreach (var entity in FixEntityIndices(text, entities))
56 var startIndex = entity.Indices[0];
57 var endIndex = entity.Indices[1];
59 if (curIndex > startIndex)
60 continue; // 区間が重複する不正なエンティティを無視する
62 if (startIndex > endIndex)
63 continue; // 区間が不正なエンティティを無視する
65 if (startIndex > text.Length || endIndex > text.Length)
66 continue; // 区間が文字列長を越えている不正なエンティティを無視する
68 if (curIndex != startIndex)
69 yield return t(e(text.Substring(curIndex, startIndex - curIndex)));
71 var targetText = text.Substring(startIndex, endIndex - startIndex);
73 if (entity is TwitterEntityUrl urlEntity)
74 yield return FormatUrlEntity(targetText, urlEntity, keepTco);
75 else if (entity is TwitterEntityHashtag hashtagEntity)
76 yield return FormatHashtagEntity(targetText, hashtagEntity);
77 else if (entity is TwitterEntityMention mentionEntity)
78 yield return FormatMentionEntity(targetText, mentionEntity);
79 else if (entity is TwitterEntityEmoji emojiEntity)
80 yield return FormatEmojiEntity(targetText, emojiEntity);
82 yield return t(e(targetText));
87 if (curIndex != text.Length)
88 yield return t(e(text.Substring(curIndex)));
92 /// エンティティの Indices をサロゲートペアを考慮して調整します
94 private static IEnumerable<TwitterEntity> FixEntityIndices(string text, IEnumerable<TwitterEntity> entities)
97 var indexOffset = 0; // サロゲートペアによる indices のズレを表す
99 foreach (var entity in entities.OrderBy(x => x.Indices[0]))
101 var startIndex = entity.Indices[0];
102 var endIndex = entity.Indices[1];
104 for (var i = curIndex; i < (startIndex + indexOffset); i++)
105 if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1]))
108 startIndex += indexOffset;
109 curIndex = startIndex;
111 for (var i = curIndex; i < (endIndex + indexOffset); i++)
112 if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1]))
115 endIndex += indexOffset;
118 entity.Indices[0] = startIndex;
119 entity.Indices[1] = endIndex;
125 private static string FormatUrlEntity(string targetText, TwitterEntityUrl entity, bool keepTco)
129 // 過去に存在した壊れたエンティティの対策
130 // 参照: https://dev.twitter.com/discussions/12628
131 if (entity.DisplayUrl == null)
133 expandedUrl = MyCommon.ConvertToReadableUrl(targetText);
134 return "<a href=\"" + e(entity.Url) + "\" title=\"" + e(expandedUrl) + "\">" + t(e(targetText)) + "</a>";
137 var linkUrl = entity.Url;
139 expandedUrl = keepTco ? linkUrl : MyCommon.ConvertToReadableUrl(entity.ExpandedUrl);
141 var mediaEntity = entity as TwitterEntityMedia;
143 var titleText = mediaEntity?.AltText ?? expandedUrl;
145 // twitter.com へのリンクは t.co を経由せずに直接リンクする (但し pic.twitter.com はそのまま)
146 if (mediaEntity == null)
148 if (entity.ExpandedUrl.StartsWith("https://twitter.com/", StringComparison.Ordinal) ||
149 entity.ExpandedUrl.StartsWith("http://twitter.com/", StringComparison.Ordinal))
151 linkUrl = entity.ExpandedUrl;
155 return "<a href=\"" + e(linkUrl) + "\" title=\"" + e(titleText) + "\">" + t(e(entity.DisplayUrl)) + "</a>";
158 private static string FormatHashtagEntity(string targetText, TwitterEntityHashtag entity)
159 => "<a class=\"hashtag\" href=\"https://twitter.com/search?q=%23" + eu(entity.Text) + "\">" + t(e(targetText)) + "</a>";
161 private static string FormatMentionEntity(string targetText, TwitterEntityMention entity)
162 => "<a class=\"mention\" href=\"https://twitter.com/" + eu(entity.ScreenName) + "\">" + t(e(targetText)) + "</a>";
164 private static string FormatEmojiEntity(string targetText, TwitterEntityEmoji entity)
166 if (!SettingManager.Local.UseTwemoji)
167 return t(e(targetText));
169 return "<img class=\"emoji\" src=\"" + e(entity.Url) + "\" alt=\"" + e(entity.Text) + "\" />";
172 // 長いのでエイリアスとして e(...), eu(...), t(...) でエスケープできるようにする
173 private static Func<string, string> e = EscapeHtml;
174 private static Func<string, string> eu = Uri.EscapeDataString;
175 private static Func<string, string> t = FilterText;
177 private static string EscapeHtml(string text)
179 // Twitter API は "<" ">" "&" だけ中途半端にエスケープした状態のテキストを返すため、
180 // これらの文字だけ一旦エスケープを解除する
181 text = text.Replace("<", "<").Replace(">", ">").Replace("&", "&");
183 var result = new StringBuilder(100);
184 foreach (var c in text)
186 // 「<」「>」「&」「"」「'」についてエスケープ処理を施す
187 // 参照: http://d.hatena.ne.jp/ockeghem/20070510/1178813849
191 result.Append("<");
194 result.Append(">");
197 result.Append("&");
200 result.Append(""");
203 result.Append("'");
211 return result.ToString();
215 /// HTML の属性値ではない、通常のテキストに対するフィルタ処理
217 private static string FilterText(string text)
219 text = text.Replace("\n", "<br>");
220 text = Regex.Replace(text, " ", " ");