diff --git a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/AlexaRequestMapper.cs b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/AlexaRequestMapper.cs index 5edec414..8d799d0f 100644 --- a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/AlexaRequestMapper.cs +++ b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/AlexaRequestMapper.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Security; using System.Xml; @@ -7,6 +8,7 @@ using Alexa.NET.Request.Type; using Alexa.NET.Response; using Bot.Builder.Community.Adapters.Alexa.Core.Attachments; +using Bot.Builder.Community.Adapters.Alexa.Core.Utility; using Microsoft.Bot.Schema; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -91,18 +93,13 @@ public SkillResponse ActivityToResponse(Activity activity, SkillRequest alexaReq return response; } - if (!SecurityElement.IsValidText(activity.Text)) - { - activity.Text = SecurityElement.Escape(activity.Text); - } - if (!string.IsNullOrEmpty(activity.Speak)) { response.Response.OutputSpeech = new SsmlOutputSpeech(activity.Speak); } else { - response.Response.OutputSpeech = new PlainTextOutputSpeech(activity.Text ?? string.Empty); + response.Response.OutputSpeech = new PlainTextOutputSpeech(NormalizeActivityText(activity.TextFormat, activity.Text)); } ProcessActivityAttachments(activity, response); @@ -116,7 +113,7 @@ public SkillResponse ActivityToResponse(Activity activity, SkillRequest alexaReq break; case InputHints.ExpectingInput: response.Response.ShouldEndSession = false; - response.Response.Reprompt = new Reprompt(activity.Text); + response.Response.Reprompt = new Reprompt(NormalizeActivityText(activity.TextFormat, activity.Text)); break; default: response.Response.ShouldEndSession = _options.ShouldEndSessionByDefault; @@ -278,6 +275,40 @@ private string StripSpeakTag(string speakText) { return speakText; } + } + + private string NormalizeActivityText(string textFormat, string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return string.Empty; + } + + // Default to plain text if it isn't specified. + if (textFormat == null) + { + textFormat = TextFormatTypes.Plain; + } + + string plainText; + if (textFormat.Equals(TextFormatTypes.Plain, StringComparison.Ordinal)) + { + plainText = text; + } + else if (textFormat.Equals(TextFormatTypes.Markdown, StringComparison.Ordinal)) + { + plainText = AlexaMarkdownToPlaintextRenderer.Render(text); + } + else // xml format or other unknown and unsupported format. + { + plainText = string.Empty; + } + + if (!SecurityElement.IsValidText(plainText)) + { + plainText = SecurityElement.Escape(plainText); + } + return plainText; } /// @@ -373,4 +404,4 @@ private ICard CreateAlexaCardFromHeroCard(HeroCard heroCard) } } } -} \ No newline at end of file +} diff --git a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Bot.Builder.Community.Adapters.Alexa.Core.csproj b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Bot.Builder.Community.Adapters.Alexa.Core.csproj index 4815fdc6..52cd99b5 100644 --- a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Bot.Builder.Community.Adapters.Alexa.Core.csproj +++ b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Bot.Builder.Community.Adapters.Alexa.Core.csproj @@ -21,6 +21,7 @@ + diff --git a/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Utility/AlexaMarkdownToPlaintextRenderer.cs b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Utility/AlexaMarkdownToPlaintextRenderer.cs new file mode 100644 index 00000000..af280c94 --- /dev/null +++ b/libraries/Bot.Builder.Community.Adapters.Alexa.Core/Utility/AlexaMarkdownToPlaintextRenderer.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.MarkedNet; + +namespace Bot.Builder.Community.Adapters.Alexa.Core.Utility +{ + /// + /// Simple Markdown renderer to turn markdown into plain text for Alexa. + /// + public static class AlexaMarkdownToPlaintextRenderer + { + private static readonly Marked _marked = new Marked(new Options { Renderer = new RemoveMarkupRenderer() }); + + public static string Render(string source) => _marked.Parse(source); + + private class RemoveMarkupRenderer : MarkdownRenderer + { + private const string ListItemMarker = "$$ListItemMarker$$"; + + public override string Blockquote(string quote) => string.Concat(Environment.NewLine, quote, Environment.NewLine); + public override string Br() => Environment.NewLine; + public override string Code(string code, string lang, bool escaped) => code; + public override string Codespan(string text) => text; + public override string Del(string text) => text; + public override string Em(string text) => text; + public override string Heading(string text, int level, string raw) => string.Concat(Environment.NewLine, text, Environment.NewLine); + public override string Hr() => Environment.NewLine; + public override string Html(string html) => string.Empty; + public override string Image(string href, string title, string text) => title ?? text; + public override string Link(string href, string title, string text) => $"{title ?? text} {href}"; + public override string List(string body, bool ordered, int start) + { + if (ordered) + { + for (int marker = start, markerIndex = body.IndexOf(ListItemMarker); markerIndex >= 0; markerIndex = body.IndexOf(ListItemMarker), ++marker) + { + body = body.Substring(0, markerIndex) + Environment.NewLine + marker + " " + body.Substring(markerIndex + ListItemMarker.Length); + } + } + else + { + body = body.Replace(ListItemMarker, Environment.NewLine); + } + return body; + } + public override string ListItem(string text) => $"{ListItemMarker}{text}"; + public override string Paragraph(string text) => string.Concat(Environment.NewLine, text, Environment.NewLine); + public override string Strong(string text) => text; + public override string Table(string header, string body) => string.Empty; + public override string TableCell(string content, TableCellFlags flags) => string.Empty; + public override string TableRow(string content) => string.Empty; + } + } +}