diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index 3cc1d69..d958ab8 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -1,19 +1,11 @@ -using System.Text; -using System.Text.Json; using System.Text.RegularExpressions; -using AngleSharp.Common; using DysonNetwork.Shared; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Registry; using DysonNetwork.Sphere.WebReader; using DysonNetwork.Sphere.Localization; -using DysonNetwork.Sphere.Poll; using DysonNetwork.Sphere.Publisher; -using Markdig; -using Markdig.Extensions.Mathematics; -using Markdig.Syntax; -using Markdig.Syntax.Inlines; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; using NodaTime; @@ -41,29 +33,25 @@ public partial class PostService( { const int maxLength = 256; const int embedMaxLength = 80; - var pipeline = new MarkdownPipelineBuilder() - .UseAdvancedExtensions() - .Build(); - foreach (var item in input) { if (item.Content?.Length > maxLength) { - item.Content = TruncateMarkdownSafely(item.Content, maxLength, pipeline); + item.Content = item.Content[..maxLength]; item.IsTruncated = true; } // Truncate replied post content with shorter embed length if (item.RepliedPost?.Content?.Length > embedMaxLength) { - item.RepliedPost.Content = TruncateMarkdownSafely(item.RepliedPost.Content, embedMaxLength, pipeline); + item.RepliedPost.Content = item.RepliedPost.Content[..embedMaxLength]; item.RepliedPost.IsTruncated = true; } // Truncate forwarded post content with shorter embed length if (item.ForwardedPost?.Content?.Length > embedMaxLength) { - item.ForwardedPost.Content = TruncateMarkdownSafely(item.ForwardedPost.Content, embedMaxLength, pipeline); + item.ForwardedPost.Content = item.ForwardedPost.Content[..embedMaxLength]; item.ForwardedPost.IsTruncated = true; } } @@ -71,397 +59,6 @@ public partial class PostService( return input; } - /// - /// Truncates markdown content safely by cutting at safe inline boundaries - /// - private static string TruncateMarkdownSafely(string markdown, int maxLength, MarkdownPipeline pipeline) - { - if (string.IsNullOrEmpty(markdown) || markdown.Length <= maxLength) - return markdown; - - var document = Markdown.Parse(markdown, pipeline); - var resultBuilder = new StringBuilder(); - var currentLength = 0; - var truncated = false; - var previousWasBlock = false; - - foreach (var block in document) - { - if (truncated) - break; - - if (block is ParagraphBlock paragraph) - { - var (truncatedPara, paraLength, wasTruncated) = - TruncateParagraph(paragraph, markdown, maxLength - currentLength); - - if (truncatedPara.Length > 0) - { - if (previousWasBlock) - { - resultBuilder.AppendLine(); - } - resultBuilder.Append(truncatedPara); - currentLength += paraLength; - previousWasBlock = true; - } - - if (wasTruncated) - { - truncated = true; - break; - } - } - else - { - var blockText = GetBlockText(block, markdown); - var blockLength = blockText.Length; - - if (currentLength + blockLength > maxLength) - { - truncated = true; - break; - } - - if (previousWasBlock) - { - resultBuilder.AppendLine(); - } - - resultBuilder.Append(blockText); - currentLength += blockLength; - previousWasBlock = true; - } - } - - var result = CollapseNewlines(resultBuilder.ToString().TrimEnd()); - - if (truncated) - { - result += "..."; - } - - return result; - } - - /// - /// Truncates a paragraph at safe inline boundaries - /// - private static (string markdown, int length, bool truncated) TruncateParagraph( - ParagraphBlock paragraph, string originalMarkdown, int remainingLength) - { - if (paragraph.Inline == null || remainingLength <= 0) - return ("", 0, false); - - var resultBuilder = new StringBuilder(); - var currentLength = 0; - var truncated = false; - - foreach (var inline in paragraph.Inline) - { - var inlineText = GetInlineText(inline, originalMarkdown); - - if (currentLength + inlineText.Length > remainingLength) - { - var availableChars = remainingLength - currentLength; - if (availableChars > 3) - { - var truncatedText = inlineText[..availableChars]; - var lastSpace = truncatedText.LastIndexOf(' '); - if (lastSpace > 0) - { - truncatedText = truncatedText[..lastSpace]; - } - resultBuilder.Append(truncatedText); - currentLength += truncatedText.Length; - } - truncated = true; - break; - } - - resultBuilder.Append(inlineText); - currentLength += inlineText.Length; - } - - return (resultBuilder.ToString(), currentLength, truncated); - } - - /// - /// Gets the markdown text representation of a block - /// - private static string GetBlockText(Block block, string originalMarkdown = "") - { - return block switch - { - ParagraphBlock paragraph => GetParagraphText(paragraph, originalMarkdown), - HeadingBlock heading => GetHeadingText(heading, originalMarkdown), - MathBlock math => GetMathBlockText(math), - FencedCodeBlock code => GetFencedCodeText(code), - CodeBlock code => GetIndentedCodeText(code), - ListBlock list => GetListText(list), - QuoteBlock quote => GetQuoteText(quote), - ThematicBreakBlock => "---", - _ => "" - }; - } - - /// - /// Gets markdown text for a paragraph block - /// - private static string GetParagraphText(ParagraphBlock paragraph, string originalMarkdown = "") - { - if (paragraph.Inline == null) return ""; - return GetInlinesText(paragraph.Inline, originalMarkdown); - } - - /// - /// Gets markdown text for a heading block - /// - private static string GetHeadingText(HeadingBlock heading, string originalMarkdown = "") - { - var prefix = new string('#', heading.Level); - var text = heading.Inline != null ? GetInlinesText(heading.Inline, originalMarkdown) : ""; - return $"{prefix} {text}"; - } - - /// - /// Gets markdown text for a fenced code block - /// - private static string GetFencedCodeText(FencedCodeBlock code) - { - var lines = code.Lines; - var sb = new StringBuilder(); - var fenceChar = code.FencedChar; - var fenceLength = code.OpeningFencedCharCount; - - if (fenceLength > 0) - { - sb.Append(new string(fenceChar, fenceLength)); - if (!string.IsNullOrEmpty(code.Info)) - { - sb.Append(code.Info); - } - sb.AppendLine(); - } - - foreach (var line in lines) - { - sb.AppendLine(line.ToString()); - } - - if (fenceLength > 0) - { - sb.Append(new string(fenceChar, fenceLength)); - } - - return sb.ToString(); - } - - /// - /// Gets markdown text for a math block (LaTeX) - /// - private static string GetMathBlockText(MathBlock math) - { - var lines = math.CodeBlockLines; - var sb = new StringBuilder(); - var fenceChar = math.FencedChar; - var fenceLength = math.OpeningFencedCharCount; - - if (fenceLength > 0) - { - sb.Append(new string(fenceChar, fenceLength)); - if (!math.UnescapedInfo.IsEmpty) - { - sb.Append(math.UnescapedInfo.ToString()); - } - sb.AppendLine(); - } - - foreach (var line in lines) - { - sb.AppendLine(line.ToString()); - } - - if (fenceLength > 0) - { - sb.Append(new string(fenceChar, fenceLength)); - } - - return sb.ToString(); - } - - /// - /// Gets markdown text for an indented code block - /// - private static string GetIndentedCodeText(CodeBlock code) - { - var lines = code.Lines; - var sb = new StringBuilder(); - - foreach (var line in lines) - { - sb.Append(" "); - sb.AppendLine(line.ToString()); - } - - return sb.ToString(); - } - - /// - /// Gets markdown text for a list block - /// - private static string GetListText(ListBlock list) - { - var sb = new StringBuilder(); - var index = 0; - - foreach (var item in list) - { - if (item is ListItemBlock listItem) - { - if (list.IsOrdered) - { - index++; - sb.Append($"{index}. "); - } - else - { - sb.Append(list.BulletType != '\0' ? list.BulletType.ToString() : "-"); - sb.Append(" "); - } - - foreach (var child in listItem) - { - sb.AppendLine(GetBlockText(child)); - } - } - } - - return sb.ToString(); - } - - /// - /// Gets markdown text for a quote block - /// - private static string GetQuoteText(QuoteBlock quote) - { - var sb = new StringBuilder(); - - foreach (var block in quote) - { - var blockText = GetBlockText(block); - var lines = blockText.Split('\n'); - foreach (var line in lines) - { - sb.Append("> "); - sb.AppendLine(line.TrimEnd()); - } - } - - return sb.ToString(); - } - - /// - /// Truncates a paragraph at safe inline boundaries - /// - private static (string markdown, int length, bool truncated) TruncateParagraph( - ParagraphBlock paragraph, int remainingLength, MarkdownPipeline pipeline) - { - if (paragraph.Inline == null || remainingLength <= 0) - return ("", 0, false); - - var resultBuilder = new StringBuilder(); - var currentLength = 0; - var truncated = false; - - foreach (var inline in paragraph.Inline) - { - var inlineText = GetInlineText(inline); - - if (currentLength + inlineText.Length > remainingLength) - { - var availableChars = remainingLength - currentLength; - if (availableChars > 3) - { - var truncatedText = inlineText[..availableChars]; - var lastSpace = truncatedText.LastIndexOf(' '); - if (lastSpace > 0) - { - truncatedText = truncatedText[..lastSpace]; - } - resultBuilder.Append(truncatedText); - currentLength += truncatedText.Length; - } - truncated = true; - break; - } - - resultBuilder.Append(inlineText); - currentLength += inlineText.Length; - } - - return (resultBuilder.ToString(), currentLength, truncated); - } - - /// - /// Gets text representation of an inline element - /// - private static string GetInlineText(Inline inline, string originalMarkdown = "") - { - return inline switch - { - LiteralInline literal => literal.Content.ToString(), - CodeInline code => $"`{code.Content}`", - EmphasisInline emph => emph.DelimiterCount == 2 ? $"**{GetInlinesText(emph, originalMarkdown)}**" : $"*{GetInlinesText(emph, originalMarkdown)}*", - LinkInline link => $"[{GetInlinesText(link, originalMarkdown)}]({link.Url})", - MathInline math => GetMathInlineText(math, originalMarkdown), - LineBreakInline => "\n", - ContainerInline container => GetInlinesText(container, originalMarkdown), - _ => "" - }; - } - - /// - /// Gets markdown text for a math inline (LaTeX) - /// - private static string GetMathInlineText(MathInline math, string originalMarkdown) - { - var delimiter = math.Delimiter; - var count = math.DelimiterCount; - var fence = new string(delimiter, count); - - // Extract content from original markdown using span - if (!string.IsNullOrEmpty(originalMarkdown) && math.Span.End - math.Span.Start > count * 2) - { - var fullContent = originalMarkdown.Substring(math.Span.Start, math.Span.End - math.Span.Start); - return fullContent; - } - - // Fallback: use delimiter + empty content - return $"{fence}{fence}"; - } - - /// - /// Gets combined text from container inline elements - /// - private static string GetInlinesText(ContainerInline container, string originalMarkdown = "") - { - var sb = new StringBuilder(); - foreach (var child in container) - { - sb.Append(GetInlineText(child, originalMarkdown)); - } - return sb.ToString(); - } - - /// - /// Collapses multiple consecutive newlines to max 2 - /// - private static string CollapseNewlines(string markdown) - { - return Regex.Replace(markdown, @"\n{3,}", "\n\n"); - } - public (string title, string content) ChopPostForNotification(SnPost post) { var content = !string.IsNullOrEmpty(post.Description) @@ -1107,7 +704,8 @@ public partial class PostService( List userFriends = []; if (currentUser is not null) { - var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest { AccountId = currentUser.Id }); + var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest + { AccountId = currentUser.Id }); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); publishers = await ps.GetUserPublishers(Guid.Parse(currentUser.Id)); } @@ -1171,7 +769,8 @@ public partial class PostService( if (currentUser is null) { // Anonymous user can only view public posts that are published - return post.PublishedAt != null && now >= post.PublishedAt && post.Visibility == Shared.Models.PostVisibility.Public; + return post.PublishedAt != null && now >= post.PublishedAt && + post.Visibility == Shared.Models.PostVisibility.Public; } // Check publication status - either published or user is member @@ -1202,7 +801,7 @@ public partial class PostService( g => g.Count() ); } - + public async Task> LoadPostInfo( List posts, Account? currentUser = null, @@ -1434,4 +1033,4 @@ public static class PostQueryExtensions (e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) || publishersId.Contains(e.PublisherId)); } -} +} \ No newline at end of file