fix(tickets): convert ticket HTML to markdown on import
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
@@ -73,7 +75,7 @@ public sealed class TicketImportService
|
||||
{
|
||||
Id = taskId,
|
||||
ListId = listId,
|
||||
Title = ticket.Title,
|
||||
Title = WebUtility.HtmlDecode(ticket.Title).Trim(),
|
||||
Description = BuildDescription(ticket),
|
||||
Status = TaskStatus.Idle,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
@@ -102,7 +104,43 @@ public sealed class TicketImportService
|
||||
|
||||
private static string BuildDescription(TicketBoardItem ticket)
|
||||
{
|
||||
var body = string.IsNullOrWhiteSpace(ticket.Description) ? "" : ticket.Description.Trim() + "\n\n";
|
||||
return body + $"Ticket #{ticket.Id}";
|
||||
var body = string.IsNullOrWhiteSpace(ticket.Description) ? "" : HtmlToMarkdown(ticket.Description);
|
||||
return (body.Length > 0 ? body + "\n\n" : "") + $"Ticket #{ticket.Id}";
|
||||
}
|
||||
|
||||
private const RegexOptions Rx = RegexOptions.IgnoreCase | RegexOptions.Singleline;
|
||||
|
||||
/// <summary>
|
||||
/// Wandelt das Rich-Text-HTML des Ticketsystems in das Markdown-Subset von <c>MarkdownView</c>.
|
||||
/// Zeilenumbrüche werden zu Absätzen, weil <c>MarkdownView</c> einfache Umbrüche zusammenzieht.
|
||||
/// </summary>
|
||||
// ponytail: Regex statt HTML-Parser — verschachtelte Listen und Tabellen werden flach; AngleSharp, falls die Tickets das brauchen
|
||||
internal static string HtmlToMarkdown(string html)
|
||||
{
|
||||
var s = Regex.Replace(html, @"\s*\r?\n\s*", " ");
|
||||
s = Regex.Replace(s, @"<(script|style)\b.*?</\1>", "", Rx);
|
||||
s = Regex.Replace(s, @"<h([1-6])\b[^>]*>(.*?)</h\1>",
|
||||
m => $"\n\n{new string('#', Math.Min(int.Parse(m.Groups[1].Value), 3))} {m.Groups[2].Value}\n\n", Rx);
|
||||
s = Regex.Replace(s, @"<(b|strong)\b[^>]*>(.*?)</\1>", m => Wrap(m.Groups[2].Value, "**"), Rx);
|
||||
s = Regex.Replace(s, @"<(i|em)\b[^>]*>(.*?)</\1>", m => Wrap(m.Groups[2].Value, "*"), Rx);
|
||||
s = Regex.Replace(s, @"<code\b[^>]*>(.*?)</code>", m => Wrap(m.Groups[1].Value, "`"), Rx);
|
||||
s = Regex.Replace(s, @"<a\b[^>]*href=""([^""]*)""[^>]*>(.*?)</a>",
|
||||
m => m.Groups[2].Value == m.Groups[1].Value ? m.Groups[1].Value : $"{m.Groups[2].Value} ({m.Groups[1].Value})", Rx);
|
||||
s = Regex.Replace(s, @"<ol\b[^>]*>(.*?)</ol>", m =>
|
||||
{
|
||||
var n = 0;
|
||||
return "\n\n" + Regex.Replace(m.Groups[1].Value, @"<li\b[^>]*>", _ => $"\n{++n}. ", Rx) + "\n\n";
|
||||
}, Rx);
|
||||
s = Regex.Replace(s, @"<li\b[^>]*>", "\n- ", Rx);
|
||||
s = Regex.Replace(s, @"</?(ul|ol)\b[^>]*>", "\n\n", Rx);
|
||||
s = Regex.Replace(s, @"<br\s*/?>|</?(p|div|blockquote|tr)\b[^>]*>", "\n\n", Rx);
|
||||
s = Regex.Replace(s, @"<[^>]+>", "");
|
||||
s = WebUtility.HtmlDecode(s).Replace((char)0xA0, ' ');
|
||||
|
||||
var lines = s.Split('\n').Select(l => Regex.Replace(l, @"[ \t]+", " ").Trim());
|
||||
return Regex.Replace(string.Join('\n', lines), @"\n{3,}", "\n\n").Trim();
|
||||
|
||||
static string Wrap(string inner, string mark)
|
||||
=> string.IsNullOrWhiteSpace(inner) ? inner : $"{mark}{inner.Trim()}{mark}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user