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.
25 using System.Collections.Generic;
26 using System.Globalization;
29 using System.Text.RegularExpressions;
30 using OpenTween.Api.DataModel;
31 using OpenTween.Setting;
36 /// ツイートの Entity 情報をもとにリンク化などを施すクラス
38 public static class TweetFormatter
40 public static string AutoLinkHtml(string text, IEnumerable<TwitterEntity>? entities, bool keepTco = false)
43 entities = Enumerable.Empty<TwitterEntity>();
45 var entitiesQuery = entities
46 .Where(x => x != null)
47 .Where(x => x.Indices != null && x.Indices.Length == 2);
49 return string.Concat(AutoLinkHtmlInternal(text, entitiesQuery, keepTco));
52 private static IEnumerable<string> AutoLinkHtmlInternal(string text, IEnumerable<TwitterEntity> entities, bool keepTco)
56 foreach (var entity in FixEntityIndices(text, entities))
58 var startIndex = entity.Indices[0];
59 var endIndex = entity.Indices[1];
61 if (curIndex > startIndex)
62 continue; // 区間が重複する不正なエンティティを無視する
64 if (startIndex > endIndex)
65 continue; // 区間が不正なエンティティを無視する
67 if (startIndex > text.Length || endIndex > text.Length)
68 continue; // 区間が文字列長を越えている不正なエンティティを無視する
70 if (curIndex != startIndex)
71 yield return t(e(text.Substring(curIndex, startIndex - curIndex)));
73 var targetText = text.Substring(startIndex, endIndex - startIndex);
75 if (entity is TwitterEntityUrl urlEntity)
76 yield return FormatUrlEntity(targetText, urlEntity, keepTco);
77 else if (entity is TwitterEntityHashtag hashtagEntity)
78 yield return FormatHashtagEntity(targetText, hashtagEntity);
79 else if (entity is TwitterEntityMention mentionEntity)
80 yield return FormatMentionEntity(targetText, mentionEntity);
81 else if (entity is TwitterEntityEmoji emojiEntity)
82 yield return FormatEmojiEntity(targetText, emojiEntity);
84 yield return t(e(targetText));
89 if (curIndex != text.Length)
90 yield return t(e(text.Substring(curIndex)));
94 /// エンティティの Indices をサロゲートペアを考慮して調整します
96 private static IEnumerable<TwitterEntity> FixEntityIndices(string text, IEnumerable<TwitterEntity> entities)
99 var indexOffset = 0; // サロゲートペアによる indices のズレを表す
101 foreach (var entity in entities.OrderBy(x => x.Indices[0]))
103 var startIndex = entity.Indices[0];
104 var endIndex = entity.Indices[1];
106 for (var i = curIndex; i < (startIndex + indexOffset); i++)
107 if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1]))
110 startIndex += indexOffset;
111 curIndex = startIndex;
113 for (var i = curIndex; i < (endIndex + indexOffset); i++)
114 if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1]))
117 endIndex += indexOffset;
120 entity.Indices[0] = startIndex;
121 entity.Indices[1] = endIndex;
127 private static string FormatUrlEntity(string targetText, TwitterEntityUrl entity, bool keepTco)
131 // 過去に存在した壊れたエンティティの対策
132 // 参照: https://dev.twitter.com/discussions/12628
133 if (entity.DisplayUrl == null)
135 expandedUrl = MyCommon.ConvertToReadableUrl(targetText);
136 return "<a href=\"" + e(entity.Url) + "\" title=\"" + e(expandedUrl) + "\">" + t(e(targetText)) + "</a>";
139 var linkUrl = entity.Url;
141 expandedUrl = keepTco ? linkUrl : MyCommon.ConvertToReadableUrl(entity.ExpandedUrl);
143 var mediaEntity = entity as TwitterEntityMedia;
145 var titleText = mediaEntity?.AltText ?? expandedUrl;
147 // twitter.com へのリンクは t.co を経由せずに直接リンクする (但し pic.twitter.com はそのまま)
148 if (mediaEntity == null)
150 if (entity.ExpandedUrl.StartsWith("https://twitter.com/", StringComparison.Ordinal) ||
151 entity.ExpandedUrl.StartsWith("http://twitter.com/", StringComparison.Ordinal))
153 linkUrl = entity.ExpandedUrl;
157 return "<a href=\"" + e(linkUrl) + "\" title=\"" + e(titleText) + "\">" + t(e(entity.DisplayUrl)) + "</a>";
160 private static string FormatHashtagEntity(string targetText, TwitterEntityHashtag entity)
161 => "<a class=\"hashtag\" href=\"https://twitter.com/search?q=%23" + eu(entity.Text) + "\">" + t(e(targetText)) + "</a>";
163 private static string FormatMentionEntity(string targetText, TwitterEntityMention entity)
164 => "<a class=\"mention\" href=\"https://twitter.com/" + eu(entity.ScreenName) + "\">" + t(e(targetText)) + "</a>";
166 private static string FormatEmojiEntity(string targetText, TwitterEntityEmoji entity)
168 if (!SettingManager.Local.UseTwemoji)
169 return t(e(targetText));
171 if (MyCommon.IsNullOrEmpty(entity.Url))
174 return "<img class=\"emoji\" src=\"" + e(entity.Url) + "\" alt=\"" + e(entity.Text) + "\" />";
177 // 長いのでエイリアスとして e(...), eu(...), t(...) でエスケープできるようにする
178 private static readonly Func<string, string> e = EscapeHtml;
179 private static readonly Func<string, string> eu = Uri.EscapeDataString;
180 private static readonly Func<string, string> t = FilterText;
182 private static string EscapeHtml(string text)
184 // Twitter API は "<" ">" "&" だけ中途半端にエスケープした状態のテキストを返すため、
185 // これらの文字だけ一旦エスケープを解除する
186 text = text.Replace("<", "<").Replace(">", ">").Replace("&", "&");
188 var result = new StringBuilder(100);
189 foreach (var c in text)
191 // 「<」「>」「&」「"」「'」についてエスケープ処理を施す
192 // 参照: http://d.hatena.ne.jp/ockeghem/20070510/1178813849
196 result.Append("<");
199 result.Append(">");
202 result.Append("&");
205 result.Append(""");
208 result.Append("'");
216 return result.ToString();
220 /// HTML の属性値ではない、通常のテキストに対するフィルタ処理
222 private static string FilterText(string text)
224 text = text.Replace("\n", "<br>");
225 text = Regex.Replace(text, " ", " ");