From 890fef4f276ec44312eb936583adbbf47d7cfe9d Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 6 Mar 2021 22:51:53 +0800 Subject: feat: Auto translate url in markdown post. --- .../IntegratedTests/TimelinePostTest.cs | 32 +++++++++++++ BackEnd/Timeline.Tests/packages.lock.json | 6 +++ .../Timeline/Controllers/TimelinePostController.cs | 15 ++++++- BackEnd/Timeline/Services/MarkdownProcessor.cs | 52 ++++++++++++++++++++++ BackEnd/Timeline/Startup.cs | 2 + BackEnd/Timeline/Timeline.csproj | 1 + BackEnd/Timeline/packages.lock.json | 6 +++ 7 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 BackEnd/Timeline/Services/MarkdownProcessor.cs diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs index b91de6c2..f00d9b13 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs @@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Jpeg; using System.Net; +using System.Text.RegularExpressions; namespace Timeline.Tests.IntegratedTests { @@ -36,6 +37,23 @@ namespace Timeline.Tests.IntegratedTests }; } + private static HttpTimelinePostCreateRequest CreateMarkdownPostRequest(string text, DateTime? time = null, string? color = null) + { + return new HttpTimelinePostCreateRequest() + { + Time = time, + Color = color, + DataList = new List() + { + new HttpTimelinePostCreateRequestData() + { + ContentType = MimeTypes.TextMarkdown, + Data = Convert.ToBase64String(Encoding.UTF8.GetBytes(text)) + } + } + }; + } + private readonly ITestOutputHelper _outputHelper; public TimelinePostTest(ITestOutputHelper outputHelper) @@ -586,5 +604,19 @@ namespace Timeline.Tests.IntegratedTests post2.Editable.Should().BeFalse(); } } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Post_Markdown_Url_Map(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateMarkdownPostRequest("[aaa](1) ![bbb](2)")); + + var res = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data"); + var markdown = await res.Content.ReadAsStringAsync(); + + markdown.Should().MatchRegex(@$"\[aaa\]\(https?://.*/timelines/{generator(1)}/posts/{post.Id}/data/1\)"); + markdown.Should().MatchRegex(@$"\[bbb\]\(https?://.*/timelines/{generator(1)}/posts/{post.Id}/data/2\)"); + } } } diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json index 50b90c3c..bdcfaf35 100644 --- a/BackEnd/Timeline.Tests/packages.lock.json +++ b/BackEnd/Timeline.Tests/packages.lock.json @@ -127,6 +127,11 @@ "System.Xml.XmlDocument": "4.3.0" } }, + "Markdig": { + "type": "Transitive", + "resolved": "0.23.0", + "contentHash": "jPPcnHGSDSedPvwZ6jiMJpvK3iJGA2djU6TFmEr6XK2BiYK1ier2lTHVwDt41XLxDbfGu5Dn42hKDzwqs049PA==" + }, "Microsoft.AspNetCore.Authorization": { "type": "Transitive", "resolved": "1.0.3", @@ -1981,6 +1986,7 @@ "dependencies": { "AutoMapper": "10.1.1", "AutoMapper.Extensions.Microsoft.DependencyInjection": "8.1.0", + "Markdig": "0.23.0", "Microsoft.AspNetCore.SpaServices.Extensions": "5.0.0", "Microsoft.EntityFrameworkCore": "5.0.0", "Microsoft.EntityFrameworkCore.Analyzers": "5.0.0", diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index 4026d551..86c5c8cf 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -32,14 +32,17 @@ namespace Timeline.Controllers private readonly TimelineMapper _timelineMapper; + private readonly MarkdownProcessor _markdownProcessor; + /// /// /// - public TimelinePostController(ITimelineService timelineService, ITimelinePostService timelinePostService, TimelineMapper timelineMapper) + public TimelinePostController(ITimelineService timelineService, ITimelinePostService timelinePostService, TimelineMapper timelineMapper, MarkdownProcessor markdownProcessor) { _timelineService = timelineService; _postService = timelinePostService; _timelineMapper = timelineMapper; + _markdownProcessor = markdownProcessor; } private bool UserHasAllTimelineManagementPermission => this.UserHasPermission(UserPermission.AllTimelineManagement); @@ -147,7 +150,15 @@ namespace Timeline.Controllers return await DataCacheHelper.GenerateActionResult(this, () => _postService.GetPostDataDigest(timelineId, post, dataIndex), - () => _postService.GetPostData(timelineId, post, dataIndex) + async () => + { + var data = await _postService.GetPostData(timelineId, post, dataIndex); + if (data.ContentType == MimeTypes.TextMarkdown) + { + return new ByteData(_markdownProcessor.Process(data.Data, Url, timeline, post), data.ContentType); + } + return data; + } ); } diff --git a/BackEnd/Timeline/Services/MarkdownProcessor.cs b/BackEnd/Timeline/Services/MarkdownProcessor.cs new file mode 100644 index 00000000..f34432cd --- /dev/null +++ b/BackEnd/Timeline/Services/MarkdownProcessor.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using Markdig; +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using Microsoft.AspNetCore.Mvc; +using Timeline.Controllers; + +namespace Timeline.Services +{ + public class MarkdownProcessor + { + public string Process(string text, Func urlGenerator) + { + MarkdownDocument markdown = Markdown.Parse(text); + foreach (var link in markdown.Descendants().Where(e => e is LinkInline).Cast()) + { + if (int.TryParse(link.Url, out var dataIndex)) + { + link.Url = urlGenerator(dataIndex); + } + } + + var writer = new StringWriter(); + NormalizeRenderer renderer = new NormalizeRenderer(writer); + renderer.Render(markdown); + + return writer.ToString(); + } + + /// Convert data url to true url with post id. + public string Process(string text, IUrlHelper url, string timeline, long post) + { + return Process( + text, + dataIndex => url.ActionLink( + nameof(TimelinePostController.DataGet), + nameof(TimelinePostController)[0..^nameof(Controller).Length], + new { timeline, post, data_index = dataIndex } + ) + ); + } + + public byte[] Process(byte[] data, IUrlHelper url, string timeline, long post) + { + return Encoding.UTF8.GetBytes(Process(Encoding.UTF8.GetString(data), url, timeline, post)); + } + } +} diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 26ba3bfc..4f392b2d 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -125,6 +125,8 @@ namespace Timeline services.AddScoped(); + services.AddScoped(); + services.AddOpenApiDocs(); if (_frontEndMode == FrontEndMode.Mock) diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj index 70536e00..6b565598 100644 --- a/BackEnd/Timeline/Timeline.csproj +++ b/BackEnd/Timeline/Timeline.csproj @@ -33,6 +33,7 @@ + all diff --git a/BackEnd/Timeline/packages.lock.json b/BackEnd/Timeline/packages.lock.json index 36442da7..da61aafc 100644 --- a/BackEnd/Timeline/packages.lock.json +++ b/BackEnd/Timeline/packages.lock.json @@ -23,6 +23,12 @@ "Microsoft.Extensions.Options": "3.0.0" } }, + "Markdig": { + "type": "Direct", + "requested": "[0.23.0, )", + "resolved": "0.23.0", + "contentHash": "jPPcnHGSDSedPvwZ6jiMJpvK3iJGA2djU6TFmEr6XK2BiYK1ier2lTHVwDt41XLxDbfGu5Dn42hKDzwqs049PA==" + }, "Microsoft.AspNetCore.SpaServices.Extensions": { "type": "Direct", "requested": "[5.0.0, )", -- cgit v1.2.3