public async Task <IActionResult> Send(long id, [FromBody] SendMailDto sendMail)
        {
            Template template;

            try
            {
                template = await context.Set <Template>()
                           .Include(x => x.Mail)
                           .FirstAsync(x => x.User.Token == sendMail.Token && x.Id == id);
            }
            catch
            {
                throw new TemplateNotFoundException();
            }

            var secret   = configuration.GetValue <string>("PasswordSecret");
            var stubble  = new StubbleBuilder().Build();
            var from     = new MailAddress(template.Mail.EmailAddress, template.Mail.Name);
            var password = encryption.Decrypt(template.Mail.Password, secret);

            foreach (var recipient in sendMail.Recipients)
            {
                var subject = await stubble.RenderAsync(template.Subject, recipient.Args);

                var body = await stubble.RenderAsync(template.Content, recipient.Args);

                var to = new MailAddress(recipient.Email);

                var message = new MailMessage(from, to)
                {
                    Subject      = subject,
                    Body         = body,
                    BodyEncoding = Encoding.UTF8,
                    IsBodyHtml   = template.IsHtml
                };

                using var smtp = new SmtpClient(template.Mail.Host, template.Mail.Port)
                      {
                          DeliveryMethod        = SmtpDeliveryMethod.Network,
                          UseDefaultCredentials = false,
                          EnableSsl             = template.Mail.EnableSsl,
                          Credentials           = new NetworkCredential(template.Mail.EmailAddress, password)
                      };
                try
                {
                    await smtp.SendMailAsync(message);
                }
                catch (Exception ex)
                {
                    throw new BusinessException("smtp-error", $"Something happened when sending the email to {recipient.Email} via SMTP: {ex.Message}", ex);
                }
            }

            return(Ok());
        }
예제 #2
0
        public async Task <Message> Render <TModel>(string subject, string body, TModel data, CancellationToken cancellationToken)
        {
            var stubble = new StubbleBuilder().Build();

            var renderedSubject = subject != null ? await stubble.RenderAsync(subject, data) : null;

            var renderedBody = body != null ? await stubble.RenderAsync(body, data) : null;

            var renderedHtmlBody = renderedBody != null?Markdig.Markdown.ToHtml(renderedBody) : null;

            return(new Message
            {
                Subject = renderedSubject,
                Body = renderedHtmlBody
            });
        }
예제 #3
0
        public async Task It_Can_Render_Literals_Async()
        {
            var stubble = new StubbleBuilder().Build();

            var output = await stubble.RenderAsync("Literal test", null);

            Assert.Equal("Literal test", output);
        }
예제 #4
0
        public async Task It_Can_Render_Inverted_Sections_Async()
        {
            var stubble = new StubbleBuilder().Build();

            var output = await stubble.RenderAsync(@"{{^True}}Not True{{/True}}", new { True = false });

            Assert.Equal("Not True", output);
        }
예제 #5
0
        public async Task It_Can_Render_The_Template_Async()
        {
            var stubble = new StubbleBuilder()
                          .Build();

            var output = await stubble.RenderAsync("{{Foo}}", new { Foo = "Bar" });

            Assert.Equal("Bar", output);
        }
예제 #6
0
        public async Task <string> FormatAsync(string template, object data)
        {
            var stubble = new StubbleBuilder().Configure(s =>
            {
                s.SetIgnoreCaseOnKeyLookup(true);
            }).Build();

            return(await stubble.RenderAsync(template, data));
        }
예제 #7
0
        private async Task <string> RenderTemplateAsync(string template, object tags)
        {
            StubbleVisitorRenderer stubble = new StubbleBuilder().Configure(x =>
            {
                x.SetIgnoreCaseOnKeyLookup(true);
                x.SetMaxRecursionDepth(512);
            }).Build();

            return(await stubble.RenderAsync(template, tags));
        }
        public async Task <IActionResult> Test(TestTemplateDto template)
        {
            Mail mail;

            try
            {
                mail = context.Set <Mail>()
                       .Where(x => x.User.Id == user.Id && x.Id == template.MailId)
                       .First();
            }
            catch (Exception ex)
            {
                throw new BusinessException("mail-not-found", "The provided mail was not found", ex);
            }

            var stubble  = new StubbleBuilder().Build();
            var from     = new MailAddress(mail.EmailAddress);
            var to       = new MailAddress(template.Recipient.Email);
            var password = encryption.Decrypt(mail.Password, template.Secret);
            var subject  = await stubble.RenderAsync(template.Subject, template.Recipient.Args);

            var body = await stubble.RenderAsync(template.Content, template.Recipient.Args);

            var message = new MailMessage(from, to)
            {
                Subject      = subject,
                Body         = body,
                BodyEncoding = Encoding.UTF8,
                IsBodyHtml   = template.IsHtml
            };

            using var smtp = new SmtpClient(mail.Host, mail.Port)
                  {
                      DeliveryMethod        = SmtpDeliveryMethod.Network,
                      UseDefaultCredentials = false,
                      EnableSsl             = mail.EnableSsl,
                      Credentials           = new NetworkCredential(mail.EmailAddress, password)
                  };

            await smtp.SendMailAsync(message);

            return(Ok());
        }
예제 #9
0
        public async Task It_Can_Render_WithPartials_FromArgs_Async()
        {
            var stubble = new StubbleBuilder().Build();

            var output = await stubble.RenderAsync("{{> foo}}", new { Foo = "Bar" }, new Dictionary <string, string>
            {
                { "foo", "{{Foo}} this" }
            });

            Assert.Equal("Bar this", output);
        }
예제 #10
0
        internal async Task <string> ApplyAsync(Dictionary <string, object> row)
        {
            if (string.IsNullOrWhiteSpace(template))
            {
                template = ReadTemplate();
            }

            var stubble = new StubbleBuilder().Build();

            return(await stubble.RenderAsync(template, row));
        }
예제 #11
0
        /// <summary>
        /// Applies the moutache rendering to the given template string.
        /// </summary>
        /// <param name="template">The template.</param>
        /// <param name="variables">The prompt results.</param>
        /// <returns>The with moustache rendering applied</returns>
        public static async Task <string> ApplyMoustache(string template, IDictionary <string, object> variables)
        {
            var renderSettings = new RenderSettings
            {
                SkipHtmlEncoding = true
            };
            var stubble = new StubbleBuilder().Build();

            return(await stubble
                   .RenderAsync(template, variables, renderSettings)
                   .ConfigureAwait(false));
        }
예제 #12
0
파일: Program.cs 프로젝트: anurse/querygen
        // Through the magic of System.CommandLine.Dragonfruit, this method is turned into a command-line API!
        // The XML docs become help text and the arguments become switches/args.
        /// <summary>
        /// Generates GitHub query lists from an input JSON file.
        /// </summary>
        /// <param name="inputFile">The JSON data file to use as input. Defaults to 'queries.json' in the current directory.</param>
        /// <param name="outputFile">The output markdown file to generate. Defaults to 'Queries.md' in the current directory.</param>
        static async Task <int> Main(string inputFile = null, string outputFile = null)
        {
            inputFile  = inputFile ?? Path.Combine(Directory.GetCurrentDirectory(), "queries.json");
            outputFile = outputFile ?? Path.Combine(Directory.GetCurrentDirectory(), "Queries.md");

            if (!File.Exists(inputFile))
            {
                Console.Error.WriteLine($"Could not find input file: {inputFile}");
                return(1);
            }

            // Load the input file
            Console.WriteLine($"Loading input data: {inputFile} ...");
            QueryList inputList;

            using (var inputStream = File.OpenRead(inputFile))
            {
                var options = new JsonSerializerOptions()
                {
                    PropertyNameCaseInsensitive = true,
                };
                options.Converters.Add(new JsonStringEnumConverter());
                inputList = await JsonSerializer.DeserializeAsync <QueryList>(inputStream, options);
            }

            // Generate queries
            Console.WriteLine("Computing queries...");
            var querySet = new QuerySet();

            foreach (var queryDefinition in inputList.Queries)
            {
                GenerateQuery(queryDefinition, inputList, querySet);
            }

            // Generate the template
            Console.WriteLine($"Generating output file: {outputFile} ...");
            var stubble = new StubbleBuilder().Build();

            using (var templateStream = typeof(Program).Assembly.GetManifestResourceStream("Internal.AspNetCore.QueryGenerator.Templates.querylist.mustache"))
                using (var templateReader = new StreamReader(templateStream))
                {
                    var template = await templateReader.ReadToEndAsync();

                    var output = await stubble.RenderAsync(template, querySet);

                    await File.WriteAllTextAsync(outputFile, output);
                }

            return(0);
        }
예제 #13
0
        private static async Task ExtractEventsFromAssemblies(string outputPath, IPlatform platform)
        {
            await platform.Extract().ConfigureAwait(false);

            Log.Debug("Extracting events from the following assemblies: {assemblies}", platform.Assemblies);

            Log.Debug("Using the following search directories: {assemblies}", platform.CecilSearchDirectories);
            var targetAssemblyDirs = platform.CecilSearchDirectories;

            var rp = new ReaderParameters
            {
                AssemblyResolver = new PathSearchAssemblyResolver(targetAssemblyDirs.ToArray())
            };

            var targetAssemblies = platform.Assemblies.Select(x => AssemblyDefinition.ReadAssembly(x, rp)).ToArray();

            Log.Debug("Using {template} as the mustache template", _mustacheTemplate);

            var namespaceData = Array.Empty <Entities.NamespaceInfo>();

            switch (platform.Platform)
            {
            case AutoPlatform.Essentials:
                namespaceData = StaticEventTemplateInformation.Create(targetAssemblies);
                break;

            default:
                namespaceData = EventTemplateInformation.Create(targetAssemblies);
                break;
            }

            var delegateData = DelegateTemplateInformation.Create(targetAssemblies);

            using (var streamReader = new StreamReader(_mustacheTemplate, Encoding.UTF8))
            {
                var template = await streamReader.ReadToEndAsync().ConfigureAwait(false);

                var stubble = new StubbleBuilder().Build();
                var result  = (await stubble.RenderAsync(template, new { Namespaces = namespaceData, DelegateNamespaces = delegateData }).ConfigureAwait(false))
                              .Replace("System.String", "string")
                              .Replace("System.Object", "object")
                              .Replace("&lt;", "<")
                              .Replace("&gt;", ">")
                              .Replace("`1", string.Empty)
                              .Replace("`2", string.Empty)
                              .Replace("`3", string.Empty);

                await File.WriteAllTextAsync(outputPath, result).ConfigureAwait(false);
            }
        }
예제 #14
0
        public async Task It_Can_Render_WithPartials_FromLoader_Async()
        {
            var stubble = new StubbleBuilder()
                          .Configure(builder => {
                builder.SetPartialTemplateLoader(new DictionaryLoader(new Dictionary <string, string>
                {
                    { "foo", "{{Foo}} this" }
                }));
            }).Build();

            var output = await stubble.RenderAsync("{{> foo}}", new { Foo = "Bar" });

            Assert.Equal("Bar this", output);
        }
예제 #15
0
        private async Task <string> ProcessTemplate(object mustacheModel, string templateUri)
        {
            var stubble = new StubbleBuilder().Build();

            var output = string.Empty;

            using (StreamReader streamReader = new StreamReader(templateUri, Encoding.UTF8))
            {
                var content = await streamReader.ReadToEndAsync();

                output = await stubble.RenderAsync(content, mustacheModel);
            }

            return(output);
        }
예제 #16
0
        public async Task It_Should_Skip_Html_Encoding_With_Setting_Async()
        {
            var stubble = new StubbleBuilder()
                          .Build();

            var obj = new
            {
                Html = "<b>Html</b>"
            };

            var result = await stubble.RenderAsync("{{Html}}\n{{{Html}}}", obj, new RenderSettings
            {
                SkipHtmlEncoding = true
            });

            Assert.Equal("<b>Html</b>\n<b>Html</b>", result);
        }
예제 #17
0
        public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken)
        {
            var source = viewLocator.GetView(model, res.HttpContext);

            if (string.IsNullOrEmpty(source))
            {
                res.StatusCode  = 500;
                res.ContentType = "text/plain";
                await res.WriteAsync("View not found", cancellationToken);
            }
            var stubble = new StubbleBuilder().Build();

            res.ContentType = "text/html";
            res.StatusCode  = (int)HttpStatusCode.OK;
            var output = await stubble.RenderAsync(source, model);

            await res.WriteAsync(output, cancellationToken);
        }
예제 #18
0
        public async Task It_Should_Allow_An_NonAsync_Render_Function_In_Lambda()
        {
            var stubble = new StubbleBuilder()
                          .Build();

            var obj = new
            {
                Value           = "a",
                ValueDictionary = new Dictionary <string, string>
                {
                    { "a", "A is Cool" },
                    { "b", "B is Cool" },
                },
                ValueRender = new Func <string, Func <string, string>, object>((string text, Func <string, string> render)
                                                                               => "{{ValueDictionary." + render("{{Value}}") + "}}")
            };

            var result = await stubble.RenderAsync("{{#ValueRender}}{{/ValueRender}}", obj);

            Assert.Equal("A is Cool", result);
        }
예제 #19
0
        public async Task It_Should_Indent_Partials_When_Folding_Literals_Correctly()
        {
            var tpl     = @"<div>
    {{> Body}}
</div>";
            var partial = @"<a href=""{{Url}}"">
    My Link
</a>";

            var stubble = new StubbleBuilder()
                          .Configure(conf =>
            {
                conf.SetPartialTemplateLoader(new DictionaryLoader(new Dictionary <string, string>
                {
                    { "Body", partial }
                }));
            })
                          .Build();

            var obj = new
            {
                Url = "MyUrl"
            };

            var result = stubble.Render(tpl, obj);

            Assert.Equal(@"<div>
    <a href=""MyUrl"">
        My Link
    </a></div>", result);

            var resultAsync = await stubble.RenderAsync(tpl, obj);

            Assert.Equal(@"<div>
    <a href=""MyUrl"">
        My Link
    </a></div>", resultAsync);
        }
예제 #20
0
        private async Task <string> RenderTemplate(string templateName, UserModel userModel, object model)
        {
            var fileIdentifier = new FileIdentifier
            {
                FileKey         = $"{templateName}.mustache",
                FolderKey       = ":templates",
                OrganizationKey = userModel.Identifier.OrganizationKey
            };

            if (!(await IsFileAccessibleAsync(fileIdentifier)))
            {
                fileIdentifier.OrganizationKey = "system";
            }

            if (!(await IsFileAccessibleAsync(fileIdentifier)))
            {
                throw new Exception($"Cannot load template for {templateName} at {fileIdentifier}");
            }

            var stubble = new StubbleBuilder().Build();

            // round-trip the model object back through
            // json so that we can get a dictionary readable by stubble
            model = JsonConvert.DeserializeObject <Dictionary <string, object> >(JsonConvert.SerializeObject(model));

            string output = null;
            await API.File.DownloadAsync(fileIdentifier, async (stream, cancel) =>
            {
                using (var streamReader = new StreamReader(stream, Encoding.UTF8))
                {
                    var content = await streamReader.ReadToEndAsync();
                    output      = await stubble.RenderAsync(content, model);
                }
            });

            return(output);
        }
예제 #21
0
        public static async Task <string> ToConsoleOutput(this QuantifierResult quantifierResult)
        {
            var stubble = new StubbleBuilder()
                          .Configure(settings => { settings.SetIgnoreCaseOnKeyLookup(true); }).Build();

            using var stream = new StreamReader(
                      Assembly.GetExecutingAssembly().GetManifestResourceStream(
                          $"{typeof(QuantifierResultExtensions).Namespace}.ConsoleOutput.mustache") !);

            var consoleOutput = await stubble.RenderAsync(
                await stream.ReadToEndAsync(),
                new
            {
                quantifierResult.Label,
                quantifierResult.QuantifiedLinesAdded,
                quantifierResult.QuantifiedLinesDeleted,
                quantifierResult.PercentileAddition,
                quantifierResult.PercentileDeletion,
                quantifierResult.FormulaPercentile,
                Formula = quantifierResult.Formula.ToString(),
            });

            return(consoleOutput);
        }
예제 #22
0
        private async Task <DirectEmailHistory> InternalSendDirectAsync(Site site,
                                                                        DirectEmailHistory history,
                                                                        IDictionary <string, string> tags)
        {
            if (history == null)
            {
                throw new ArgumentNullException(nameof(history));
            }

            var emailBase = await GetEmailBase(history.EmailBaseId, history.LanguageId);

            if (emailBase.EmailBaseText == null)
            {
                emailBase = await GetEmailBase(history.EmailBaseId,
                                               await _languageService.GetDefaultLanguageIdAsync());
            }

            var stubble = new StubbleBuilder().Build();

            using var message = new MimeMessage
                  {
                      Subject = history.Subject,
                      Body    = new BodyBuilder
                      {
                          TextBody = await stubble
                                     .RenderAsync(emailBase.EmailBaseText.TemplateText, tags),
                          HtmlBody = await stubble
                                     .RenderAsync(emailBase.EmailBaseText.TemplateHtml,
                                                  tags,
                                                  new Stubble.Core.Settings.RenderSettings { SkipHtmlEncoding = true })
                      }.ToMessageBody()
                  };

            message.From.Add(new MailboxAddress(history.FromName, history.FromEmailAddress));

            try
            {
                if (!string.IsNullOrEmpty(history.OverrideToEmailAddress))
                {
                    message.To.Add(MailboxAddress.Parse(history.OverrideToEmailAddress));
                }
                else
                {
                    message.To.Add(new MailboxAddress(history.ToName, history.ToEmailAddress));
                }
            }
            catch (ParseException ex)
            {
                _logger.LogError("Unable to parse email address: {EmailAddress}",
                                 history.ToEmailAddress);
                throw new GraException($"Unable to parse email address: {history.ToEmailAddress}", ex);
            }

            using var client = new SmtpClient
                  {
                      // accept any STARTTLS certificate
                      ServerCertificateValidationCallback = (_, __, ___, ____) => true
                  };

            client.MessageSent += (_, e) =>
            {
                history.SentResponse = e.Response?.Length > 255
                    ? e.Response[..255]
예제 #23
0
        public async Task <DirectEmailHistory> SendDirectAsync(DirectEmailDetails directEmailDetails)
        {
            if (directEmailDetails == null)
            {
                throw new ArgumentNullException(nameof(directEmailDetails));
            }

            string toAddress;
            string toName;
            int    languageId;
            Site   site;

            if (directEmailDetails.ToUserId.HasValue)
            {
                var user = await _userRepository.GetByIdAsync(directEmailDetails.ToUserId
                                                              ?? directEmailDetails.SendingUserId);

                if (string.IsNullOrEmpty(user.Email))
                {
                    _logger.LogError("Unable to send email to user id {UserId}: no email address configured.",
                                     directEmailDetails.ToUserId);
                    throw new GraException($"User id {directEmailDetails.ToUserId} does not have an email address configured.");
                }
                site = await _siteLookupService.GetByIdAsync(user.SiteId);

                toAddress  = user.Email;
                toName     = user.FullName;
                languageId = directEmailDetails.LanguageId ?? (string.IsNullOrEmpty(user.Culture)
                        ? await _languageService.GetDefaultLanguageIdAsync()
                        : await _languageService.GetLanguageIdAsync(user.Culture));
            }
            else
            {
                var user = await _userRepository.GetByIdAsync(directEmailDetails.SendingUserId);

                site = await _siteLookupService.GetByIdAsync(user.SiteId);

                toAddress  = directEmailDetails.ToAddress;
                toName     = directEmailDetails.ToName;
                languageId = directEmailDetails.LanguageId
                             ?? await _languageService.GetLanguageIdAsync(CultureInfo.CurrentCulture.Name);
            }

            if (!SiteCanSendMail(site))
            {
                throw new GraException("Unable to send mail, please ensure from name, from email, and outgoing mail server are configured in Site Management -> Configuration.");
            }

            var history = new DirectEmailHistory
            {
                CreatedBy              = directEmailDetails.SendingUserId,
                FromEmailAddress       = site.FromEmailAddress,
                FromName               = site.FromEmailName,
                IsBulk                 = directEmailDetails.IsBulk,
                LanguageId             = languageId,
                OverrideToEmailAddress = string
                                         .IsNullOrWhiteSpace(_config[ConfigurationKey.EmailOverride])
                    ? null
                    : _config[ConfigurationKey.EmailOverride],
                ToEmailAddress = toAddress,
                ToName         = toName
            };

            if (directEmailDetails.ToUserId.HasValue)
            {
                history.UserId = directEmailDetails.ToUserId.Value;
            }

            DirectEmailTemplate directEmailTemplate
                = await GetDirectEmailTemplateAsync(directEmailDetails.DirectEmailSystemId,
                                                    directEmailDetails.DirectEmailTemplateId,
                                                    history.LanguageId);

            if (directEmailTemplate == null || directEmailTemplate.DirectEmailTemplateText == null)
            {
                // not available in the requested language, use the default language
                history.LanguageId = await _languageService.GetDefaultLanguageIdAsync();

                directEmailTemplate
                    = await GetDirectEmailTemplateAsync(directEmailDetails.DirectEmailSystemId,
                                                        directEmailDetails.DirectEmailTemplateId,
                                                        history.LanguageId);
            }

            history.DirectEmailTemplateId = directEmailTemplate.Id;
            history.EmailBaseId           = directEmailTemplate.EmailBaseId;

            var stubble = new StubbleBuilder().Build();

            history.Subject = await stubble
                              .RenderAsync(directEmailTemplate.DirectEmailTemplateText.Subject,
                                           directEmailDetails.Tags);

            history.BodyText = await stubble
                               .RenderAsync(directEmailTemplate.DirectEmailTemplateText.BodyCommonMark,
                                            directEmailDetails.Tags);

            history.BodyHtml = CommonMark.CommonMarkConverter.Convert(history.BodyText);

            string preview = await stubble
                             .RenderAsync(directEmailTemplate.DirectEmailTemplateText.Preview,
                                          directEmailDetails.Tags);

            string title = await stubble
                           .RenderAsync(directEmailTemplate.DirectEmailTemplateText.Title,
                                        directEmailDetails.Tags);

            string footer = CommonMark.CommonMarkConverter.Convert(await stubble
                                                                   .RenderAsync(directEmailTemplate.DirectEmailTemplateText.Footer,
                                                                                directEmailDetails.Tags));

            history = await InternalSendDirectAsync(site,
                                                    history,
                                                    new Dictionary <string, string>
            {
                { "Footer", footer },
                { "Preview", preview },
                { "Title", title },
                { "BodyHtml", history.BodyHtml },
                { "BodyText", history.BodyText }
            });

            if (directEmailDetails.IsBulk && !directEmailDetails.IsTest)
            {
                if (directEmailDetails.ToUserId.HasValue)
                {
                    history.BodyHtml = null;
                    history.BodyText = null;
                    await _directEmailHistoryRepository.AddSaveNoAuditAsync(history);
                }
                if (!directEmailTemplate.SentBulk)
                {
                    await _directEmailTemplateRepository
                    .UpdateSentBulkAsync(directEmailTemplate.Id);
                }
            }
            else
            {
                if (!directEmailDetails.IsTest)
                {
                    await _directEmailHistoryRepository.AddSaveNoAuditAsync(history);
                }
                await IncrementSentCountAsync(directEmailTemplate.Id);
            }

            return(history);
        }
예제 #24
0
        public static async Task <string> ToMarkdownCommentAsync(
            this QuantifierResult quantifierResult,
            string repositoryLink,
            string contextFileLink,
            string pullRequestLink,
            string authorName,
            MarkdownCommentOptions markdownCommentOptions = null)
        {
            markdownCommentOptions ??= new MarkdownCommentOptions();

            var stubble = new StubbleBuilder()
                          .Configure(settings => { settings.SetIgnoreCaseOnKeyLookup(true); }).Build();

            using var stream = new StreamReader(
                      Assembly.GetExecutingAssembly().GetManifestResourceStream(
                          $"{typeof(QuantifierResultExtensions).Namespace}.QuantifierComment.mustache") !);

            var feedbackLinkThumbsUp = CreateFeedbackLink(
                "ThumbsUp",
                authorName,
                repositoryLink,
                pullRequestLink);
            var feedbackLinkNeutral = CreateFeedbackLink(
                "Neutral",
                authorName,
                repositoryLink,
                pullRequestLink);
            var feedbackLinkThumbsDown = CreateFeedbackLink(
                "ThumbsDown",
                authorName,
                repositoryLink,
                pullRequestLink);

            var comment = await stubble.RenderAsync(
                await stream.ReadToEndAsync(),
                new
            {
                quantifierResult.Color,
                quantifierResult.Label,
                quantifierResult.QuantifiedLinesAdded,
                quantifierResult.QuantifiedLinesDeleted,
                quantifierResult.PercentileAddition,
                quantifierResult.PercentileDeletion,
                quantifierResult.FormulaPercentile,
                Formula             = quantifierResult.Formula.ToString(),
                ContextFileLink     = contextFileLink,
                FeedbackLinkUp      = feedbackLinkThumbsUp,
                FeedbackLinkNeutral = feedbackLinkNeutral,
                FeedbackLinkDown    = feedbackLinkThumbsDown,
                TotalFilesChanged   = quantifierResult.QuantifierInput.Changes.Count,
                Details             = string.Join(
                    Environment.NewLine,
                    quantifierResult.QuantifierInput.Changes
                    .GroupBy(c => c.FileExtension)
                    .Select(
                        g =>
                        $"{g.Key} : +{g.Sum(v => v.QuantifiedLinesAdded)} -{g.Sum(v => v.QuantifiedLinesDeleted)}")),
                CollapseChangesSummarySection        = markdownCommentOptions.CollapseChangesSummarySection ? string.Empty : "open",
                CollapsePullRequestQuantifiedSection = markdownCommentOptions.CollapsePullRequestQuantifiedSection ? string.Empty : "open"
            });

            return(comment);
        }
        public static async Task <string> ToMarkdownCommentAsync(
            this QuantifierResult quantifierResult,
            string repositoryLink,
            string contextFileLink,
            string pullRequestLink,
            string authorName,
            bool anonymous = true,
            MarkdownCommentOptions markdownCommentOptions = null)
        {
            markdownCommentOptions ??= new MarkdownCommentOptions();

            var stubble = new StubbleBuilder()
                          .Configure(settings => { settings.SetIgnoreCaseOnKeyLookup(true); }).Build();

            using var stream = new StreamReader(
                      Assembly.GetExecutingAssembly().GetManifestResourceStream(
                          $"{typeof(QuantifierResultExtensions).Namespace}.Mustache.QuantifierComment.mustache") !);

            var feedbackLinkThumbsUp = CreateFeedbackLink(
                "ThumbsUp",
                authorName,
                repositoryLink,
                pullRequestLink,
                anonymous);
            var feedbackLinkNeutral = CreateFeedbackLink(
                "Neutral",
                authorName,
                repositoryLink,
                pullRequestLink,
                anonymous);
            var feedbackLinkThumbsDown = CreateFeedbackLink(
                "ThumbsDown",
                authorName,
                repositoryLink,
                pullRequestLink,
                anonymous);

            var contextFormulaPercentile =
                quantifierResult.Context.FormulaPercentile.First(f => f.Item1 == quantifierResult.Formula).Item2;

            var(idealSizeLowerBound, idealSizeUpperBound) = GetIdealChangeCountRange(contextFormulaPercentile);
            var detailsByFileExt = quantifierResult.QuantifierInput.Changes
                                   .Where(c => !string.IsNullOrEmpty(c.FileExtension))
                                   .GroupBy(c => c.FileExtension)
                                   .Select(
                g =>
                $"{g.Key} : +{g.Sum(v => v.QuantifiedLinesAdded)} -{g.Sum(v => v.QuantifiedLinesDeleted)}")
                                   .Concat(
                quantifierResult.QuantifierInput.Changes
                .Where(c => string.IsNullOrEmpty(c.FileExtension))
                .Select(c => $"{c.FilePath} : +{c.QuantifiedLinesAdded} -{c.QuantifiedLinesDeleted}"));
            var comment = await stubble.RenderAsync(
                await stream.ReadToEndAsync(),
                new
            {
                // lower th color str because of a bug in shield service
                // https://img.shields.io/static/v1?label=Quantified&message=Extra%20Small&color=Red is not respecting the red color,
                // but https://img.shields.io/static/v1?label=Quantified&message=Extra%20Small&color=red is working
                Color = quantifierResult.Color.ToLower(),
                quantifierResult.Label,

                // encode the label in case contains space
                EncodedLabel = Uri.EscapeUriString(quantifierResult.Label),
                quantifierResult.QuantifiedLinesAdded,
                quantifierResult.QuantifiedLinesDeleted,
                quantifierResult.FormulaLinesChanged,
                quantifierResult.PercentileAddition,
                quantifierResult.PercentileDeletion,
                quantifierResult.FormulaPercentile,
                IdealSizeLowerBound = idealSizeLowerBound,
                IdealSizeUpperBound = idealSizeUpperBound,
                IsIdealSize         = quantifierResult.FormulaLinesChanged >= idealSizeLowerBound && quantifierResult.FormulaLinesChanged <= idealSizeUpperBound,
                Formula             = quantifierResult.Formula.ToString(),
                ContextFileLink     = !string.IsNullOrWhiteSpace(contextFileLink)
                        ? contextFileLink :
                                      "https://github.com/microsoft/PullRequestQuantifier/blob/main/docs/prquantifier-yaml.md",
                FeedbackLinkUp      = feedbackLinkThumbsUp,
                FeedbackLinkNeutral = feedbackLinkNeutral,
                FeedbackLinkDown    = feedbackLinkThumbsDown,
                TotalFilesChanged   = quantifierResult.QuantifierInput.Changes.Count,
                Details             = string.Join(Environment.NewLine, detailsByFileExt),
                CollapsePullRequestQuantifiedSection = markdownCommentOptions.CollapsePullRequestQuantifiedSection ? string.Empty : "open"
            });

            return(comment);
        }