--- /dev/null
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~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 <http://www.gnu.org/licenses/>, or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+using System.Xml.Linq;
+using System.Xml.XPath;
+using Xunit;
+
+namespace OpenTween.Models
+{
+ public class DetailsHtmlBuilderTest
+ {
+ [Fact]
+ public void Build_Test()
+ {
+ var settingCommon = new SettingCommon();
+ var settingLocal = new SettingLocal();
+ using var theme = new ThemeManager(settingLocal);
+
+ var builder = new DetailsHtmlBuilder();
+ builder.Prepare(settingCommon, theme);
+
+ var actualHtml = builder.Build("tetete");
+ var parsedDocument = XDocument.Parse(actualHtml);
+ Assert.Equal("tetete", parsedDocument.XPathSelectElement("/html/body/p").Value);
+ }
+ }
+}
--- /dev/null
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~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 <http://www.gnu.org/licenses/>, or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+using System;
+using System.Drawing;
+
+namespace OpenTween.Models
+{
+ public class DetailsHtmlBuilder
+ {
+ private const string TemplateHead =
+ """<head><meta http-equiv="X-UA-Compatible" content="IE=8" />"""
+ + """<style type="text/css"><!-- """
+ + "body, p, pre {margin: 0;} "
+ + """body {font-family: "%FONT_FAMILY%", "Segoe UI Emoji", sans-serif; font-size: %FONT_SIZE%pt; background-color:rgb(%BG_COLOR%); word-wrap: break-word; color:rgb(%FONT_COLOR%);} """
+ + "pre {font-family: inherit;} "
+ + "a:link, a:visited, a:active, a:hover {color:rgb(%LINK_COLOR%); } "
+ + "img.emoji {width: 1em; height: 1em; margin: 0 .05em 0 .1em; vertical-align: -0.1em; border: none;} "
+ + ".quote-tweet {border: 1px solid #ccc; margin: 1em; padding: 0.5em;} "
+ + ".quote-tweet.reply {border-color: rgb(%BG_REPLY_COLOR%);} "
+ + ".quote-tweet-link {color: inherit !important; text-decoration: none;}"
+ + "--></style>"
+ + "</head>";
+
+ private const string TemplateMonospaced =
+ $"<html>{TemplateHead}<body><pre>%CONTENT_HTML%</pre></body></html>";
+
+ private const string TemplateProportional =
+ $"<html>{TemplateHead}<body><p>%CONTENT_HTML%</p></body></html>";
+
+ private string? preparedTemplate = null;
+
+ public void Prepare(SettingCommon settingCommon, ThemeManager theme)
+ {
+ var htmlTemplate = settingCommon.IsMonospace ? TemplateMonospaced : TemplateProportional;
+
+ static string ColorToRGBString(Color color)
+ => $"{color.R},{color.G},{color.B}";
+
+ this.preparedTemplate = htmlTemplate
+ .Replace("%FONT_FAMILY%", theme.FontDetail.Name)
+ .Replace("%FONT_SIZE%", theme.FontDetail.Size.ToString())
+ .Replace("%FONT_COLOR%", ColorToRGBString(theme.ColorDetail))
+ .Replace("%LINK_COLOR%", ColorToRGBString(theme.ColorDetailLink))
+ .Replace("%BG_COLOR%", ColorToRGBString(theme.ColorDetailBackcolor))
+ .Replace("%BG_REPLY_COLOR%", ColorToRGBString(theme.ColorAtTo));
+ }
+
+ public string Build(string contentHtml)
+ {
+ if (this.preparedTemplate == null)
+ throw new InvalidOperationException("Template is not prepared.");
+
+ return this.preparedTemplate.Replace("%CONTENT_HTML%", contentHtml);
+ }
+ }
+}
private readonly object syncObject = new(); // ロック用
- private const string DetailHtmlFormatHead =
- """<head><meta http-equiv="X-UA-Compatible" content="IE=8">"""
- + """<style type="text/css"><!-- """
- + "body, p, pre {margin: 0;} "
- + """body {font-family: "%FONT_FAMILY%", "Segoe UI Emoji", sans-serif; font-size: %FONT_SIZE%pt; background-color:rgb(%BG_COLOR%); word-wrap: break-word; color:rgb(%FONT_COLOR%);} """
- + "pre {font-family: inherit;} "
- + "a:link, a:visited, a:active, a:hover {color:rgb(%LINK_COLOR%); } "
- + "img.emoji {width: 1em; height: 1em; margin: 0 .05em 0 .1em; vertical-align: -0.1em; border: none;} "
- + ".quote-tweet {border: 1px solid #ccc; margin: 1em; padding: 0.5em;} "
- + ".quote-tweet.reply {border-color: rgb(%BG_REPLY_COLOR%);} "
- + ".quote-tweet-link {color: inherit !important; text-decoration: none;}"
- + "--></style>"
- + "</head>";
-
- private const string DetailHtmlFormatTemplateMono =
- $"<html>{DetailHtmlFormatHead}<body><pre>%CONTENT_HTML%</pre></body></html>";
-
- private const string DetailHtmlFormatTemplateNormal =
- $"<html>{DetailHtmlFormatHead}<body><p>%CONTENT_HTML%</p></body></html>";
-
- private string detailHtmlFormatPreparedTemplate = null!;
+ private readonly DetailsHtmlBuilder detailsHtmlBuilder = new();
private bool myStatusError = false;
private bool myStatusOnline = false;
// フォント&文字色&背景色保持
this.themeManager = new(this.settings.Local);
- this.tweetDetailsView.Initialize(this, this.iconCache, this.themeManager);
+ this.tweetDetailsView.Initialize(this, this.iconCache, this.themeManager, this.detailsHtmlBuilder);
// StringFormatオブジェクトへの事前設定
this.sfTab.Alignment = StringAlignment.Center;
this.sfTab.LineAlignment = StringAlignment.Center;
- this.InitDetailHtmlFormat();
+ this.detailsHtmlBuilder.Prepare(this.settings.Common, this.themeManager);
this.tweetDetailsView.ClearPostBrowser();
this.recommendedStatusFooter = " [TWNv" + Regex.Replace(MyCommon.FileVersion.Replace(".", ""), "^0*", "") + "]";
}
}
- private void InitDetailHtmlFormat()
- {
- var htmlTemplate = this.settings.Common.IsMonospace ? DetailHtmlFormatTemplateMono : DetailHtmlFormatTemplateNormal;
-
- static string ColorToRGBString(Color color)
- => $"{color.R},{color.G},{color.B}";
-
- this.detailHtmlFormatPreparedTemplate = htmlTemplate
- .Replace("%FONT_FAMILY%", this.themeManager.FontDetail.Name)
- .Replace("%FONT_SIZE%", this.themeManager.FontDetail.Size.ToString())
- .Replace("%FONT_COLOR%", ColorToRGBString(this.themeManager.ColorDetail))
- .Replace("%LINK_COLOR%", ColorToRGBString(this.themeManager.ColorDetailLink))
- .Replace("%BG_COLOR%", ColorToRGBString(this.themeManager.ColorDetailBackcolor))
- .Replace("%BG_REPLY_COLOR%", ColorToRGBString(this.themeManager.ColorAtTo));
- }
-
private void ListTab_DrawItem(object sender, DrawItemEventArgs e)
{
string txt;
try
{
- this.InitDetailHtmlFormat();
+ this.detailsHtmlBuilder.Prepare(this.settings.Common, this.themeManager);
}
catch (Exception ex)
{
this.DispSelectedPost();
}
- public string CreateDetailHtml(string orgdata)
- => this.detailHtmlFormatPreparedTemplate.Replace("%CONTENT_HTML%", orgdata);
-
private void DispSelectedPost()
=> this.DispSelectedPost(false);
private async Task DoShowUserStatus(TwitterUser user)
{
- using var userDialog = new UserInfoDialog(this, this.tw.Api);
+ using var userDialog = new UserInfoDialog(this, this.tw.Api, this.detailsHtmlBuilder);
var showUserTask = userDialog.ShowUserAsync(user);
userDialog.ShowDialog(this);
private ImageCache IconCache
=> this.iconCache ?? throw this.NotInitializedException();
+ private DetailsHtmlBuilder HtmlBuilder
+ => this.detailsHtmlBuilder ?? throw this.NotInitializedException();
+
/// <summary><see cref="PostClass"/> のダンプを表示するか</summary>
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
private TweenMain? owner;
private ImageCache? iconCache;
private ThemeManager? themeManager;
+ private DetailsHtmlBuilder? detailsHtmlBuilder;
public TweetDetailsView()
{
this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する
}
- public void Initialize(TweenMain owner, ImageCache iconCache, ThemeManager themeManager)
+ public void Initialize(TweenMain owner, ImageCache iconCache, ThemeManager themeManager, DetailsHtmlBuilder detailsHtmlBuilder)
{
this.owner = owner;
this.iconCache = iconCache;
this.themeManager = themeManager;
+ this.detailsHtmlBuilder = detailsHtmlBuilder;
}
private Exception NotInitializedException()
=> new InvalidOperationException("Cannot call before initialization");
public void ClearPostBrowser()
- => this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml("");
+ => this.PostBrowser.DocumentText = this.HtmlBuilder.Build("");
public async Task ShowPostDetails(PostClass post)
{
}
sb.Append("-----End PostClass Dump<br>");
- this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(sb.ToString());
+ this.PostBrowser.DocumentText = this.HtmlBuilder.Build(sb.ToString());
return;
}
using (ControlTransaction.Update(this.PostBrowser))
{
this.PostBrowser.DocumentText =
- this.Owner.CreateDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text);
+ this.HtmlBuilder.Build(post.IsDeleted ? "(DELETED)" : post.Text);
this.PostBrowser.Document.Window.ScrollTo(0, 0);
}
var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml;
using (ControlTransaction.Update(this.PostBrowser))
- this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
+ this.PostBrowser.DocumentText = this.HtmlBuilder.Build(body);
// 引用ツイートを読み込み
var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList();
body = post.Text + string.Concat(quoteHtmls);
using (ControlTransaction.Update(this.PostBrowser))
- this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
+ this.PostBrowser.DocumentText = this.HtmlBuilder.Build(body);
}
private async Task<string> CreateQuoteTweetHtml(PostId statusId, bool isReply)
langFrom: null,
langTo: SettingManager.Instance.Common.TranslateLanguage);
- this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(translatedText);
+ this.PostBrowser.DocumentText = this.HtmlBuilder.Build(translatedText);
}
catch (WebApiException e)
{
using OpenTween.Api;
using OpenTween.Api.DataModel;
using OpenTween.Connection;
+using OpenTween.Models;
namespace OpenTween
{
private readonly TweenMain mainForm;
private readonly TwitterApi twitterApi;
+ private readonly DetailsHtmlBuilder detailsHtmlBuilder;
- public UserInfoDialog(TweenMain mainForm, TwitterApi twitterApi)
+ public UserInfoDialog(TweenMain mainForm, TwitterApi twitterApi, DetailsHtmlBuilder detailsHtmlBuilder)
{
this.mainForm = mainForm;
this.twitterApi = twitterApi;
+ this.detailsHtmlBuilder = detailsHtmlBuilder;
this.InitializeComponent();
.Concat(TweetExtractor.ExtractEmojiEntities(descriptionText));
var html = TweetFormatter.AutoLinkHtml(descriptionText, mergedEntities);
- html = this.mainForm.CreateDetailHtml(html);
+ html = this.detailsHtmlBuilder.Build(html);
if (cancellationToken.IsCancellationRequested)
return;
}
else
{
- this.DescriptionBrowser.DocumentText = this.mainForm.CreateDetailHtml("");
+ this.DescriptionBrowser.DocumentText = this.detailsHtmlBuilder.Build("");
}
}
var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(status.FullText));
var html = TweetFormatter.AutoLinkHtml(status.FullText, mergedEntities);
- html = this.mainForm.CreateDetailHtml(html +
+ html = this.detailsHtmlBuilder.Build(html +
" Posted at " + MyCommon.DateTimeParse(status.CreatedAt).ToLocalTimeString() +
" via " + status.Source);
}
else
{
- this.RecentPostBrowser.DocumentText = this.mainForm.CreateDetailHtml(Properties.Resources.ShowUserInfo2);
+ this.RecentPostBrowser.DocumentText = this.detailsHtmlBuilder.Build(Properties.Resources.ShowUserInfo2);
}
}