Browse Source

Introduce message text formatting

pull/2324/head
Armano den Boef 3 years ago
parent
commit
2ed4e25e6f
7 changed files with 973 additions and 57 deletions
  1. +101
    -0
      src/Discord.Net.Core/Entities/Messages/Builders/CodeLanguage.cs
  2. +64
    -0
      src/Discord.Net.Core/Entities/Messages/Builders/HeaderFormat.cs
  3. +56
    -48
      src/Discord.Net.Core/Entities/Messages/Builders/MessageBuilder.cs
  4. +125
    -0
      src/Discord.Net.Core/Entities/Messages/Builders/MultiLineBuilder.cs
  5. +493
    -0
      src/Discord.Net.Core/Entities/Messages/Builders/TextBuilder.cs
  6. +101
    -0
      src/Discord.Net.Core/Extensions/MarkdownExtensions.cs
  7. +33
    -9
      src/Discord.Net.Core/Format.cs

+ 101
- 0
src/Discord.Net.Core/Entities/Messages/Builders/CodeLanguage.cs View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a language in which codeblocks can be formatted.
/// </summary>
public struct CodeLanguage
{
/// <summary>
/// Gets the tag of the language.
/// </summary>
public string Tag { get; }

/// <summary>
/// Gets the name of the language. <see cref="string.Empty"/> if this <see cref="CodeLanguage"/> was constructed with no name provided.
/// </summary>
public string Name { get; } = string.Empty;

/// <summary>
/// Gets the CSharp language format.
/// </summary>
public static readonly CodeLanguage CSharp = new("cs", "csharp");

/// <summary>
/// Gets the Javascript language format.
/// </summary>
public static readonly CodeLanguage JavaScript = new("js", "javascript");

/// <summary>
/// Gets the XML language format.
/// </summary>
public static readonly CodeLanguage XML = new("xml", "xml");

/// <summary>
/// Gets the HTML language format.
/// </summary>
public static readonly CodeLanguage HTML = new("html", "html");

/// <summary>
/// Gets the CSS markdown format.
/// </summary>
public static readonly CodeLanguage CSS = new("css", "css");

/// <summary>
/// Gets a language format that represents none.
/// </summary>
public static readonly CodeLanguage None = new("", "none");

/// <summary>
/// Creates a new language format with name & tag.
/// </summary>
/// <param name="tag">The tag with which markdown will be formatted.</param>
/// <param name="name">The name of the language.</param>
public CodeLanguage(string tag, string name)
{
Tag = tag;
Name = name;
}

/// <summary>
/// Creates a new language format with a tag.
/// </summary>
/// <param name="tag">The tag with which markdown will be formatted.</param>
public CodeLanguage(string tag)
=> Tag = tag;

/// <summary>
/// Gets the tag of the language.
/// </summary>
/// <param name="language"></param>
public static implicit operator string(CodeLanguage language)
=> language.Tag;

/// <summary>
/// Gets a language based on the tag.
/// </summary>
/// <param name="tag"></param>
public static implicit operator CodeLanguage(string tag)
=> new(tag);

/// <summary>
/// Creates markdown format for this language.
/// </summary>
/// <param name="input">The input string to format.</param>
/// <returns>A markdown formatted code-block with this language.</returns>
public string ToMarkdown(string input)
=> $"```{Tag}\n{input}\n```";

/// <summary>
/// Gets the tag of the language.
/// </summary>
/// <returns><see cref="Tag"/></returns>
public override string ToString()
=> $"{Tag}";
}
}

+ 64
- 0
src/Discord.Net.Core/Entities/Messages/Builders/HeaderFormat.cs View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents the format in which a markdown header should be presented.
/// </summary>
public readonly struct HeaderFormat
{
public string Format { get; }

/// <summary>
/// The biggest header type.
/// </summary>
public static readonly HeaderFormat H1 = new("#");

/// <summary>
/// An above-average sized header.
/// </summary>
public static readonly HeaderFormat H2 = new("##");

/// <summary>
/// An average-sized header.
/// </summary>
public static readonly HeaderFormat H3 = new("###");

/// <summary>
/// A subheader.
/// </summary>
public static readonly HeaderFormat H4 = new("####");

/// <summary>
/// A smaller subheader.
/// </summary>
public static readonly HeaderFormat H5 = new("#####");

/// <summary>
/// Slightly bigger than regular bold markdown.
/// </summary>
public static readonly HeaderFormat H6 = new("######");

private HeaderFormat(string format)
=> Format = format;

/// <summary>
/// Formats this header into markdown, appending provided string.
/// </summary>
/// <param name="input">The string to turn into a header.</param>
/// <returns>A markdown formatted header title.</returns>
public string ToMarkdown(string input)
=> $"{Format} {input}";

/// <summary>
/// Gets the markdown format for this header.
/// </summary>
/// <returns>The markdown format for this header.</returns>
public override string ToString()
=> $"{Format}";
}
}

src/Discord.Net.Core/Entities/Messages/MessageBuilder.cs → src/Discord.Net.Core/Entities/Messages/Builders/MessageBuilder.cs View File

@@ -8,35 +8,40 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic message builder that can build <see cref="Message"/>s.
/// Represents a generic message builder that can build <see cref="Message"/>'s.
/// </summary>
public class MessageBuilder
{
private string _content;
private List<ISticker> _stickers = new();
private List<EmbedBuilder> _embeds = new();
private List<FileAttachment> _files = new();
private readonly List<FileAttachment> _files;

private List<ISticker> _stickers;
private List<EmbedBuilder> _embeds;

/// <summary>
/// Gets or sets the content of this message
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">The content is bigger than the <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public string Content
{
get => _content;
set
{
if (_content?.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(value), $"Message size must be less than or equal to {DiscordConfig.MaxMessageSize} characters");

_content = value;
}
}
public TextBuilder Content { get; set; }

/// <summary>
/// Gets or sets whether or not this message is TTS.
/// </summary>
public bool IsTTS { get; set; }

/// <summary>
/// Gets or sets the allowed mentions of this message.
/// </summary>
public AllowedMentions AllowedMentions { get; set; }

/// <summary>
/// Gets or sets the message reference (reply to) of this message.
/// </summary>
public MessageReference MessageReference { get; set; }

/// <summary>
/// Gets or sets the components of this message.
/// </summary>
public ComponentBuilder Components { get; set; }

/// <summary>
/// Gets or sets the embeds of this message.
/// </summary>
@@ -52,21 +57,6 @@ namespace Discord
}
}

/// <summary>
/// Gets or sets the allowed mentions of this message.
/// </summary>
public AllowedMentions AllowedMentions { get; set; }

/// <summary>
/// Gets or sets the message reference (reply to) of this message.
/// </summary>
public MessageReference MessageReference { get; set; }

/// <summary>
/// Gets or sets the components of this message.
/// </summary>
public ComponentBuilder Components { get; set; } = new();

/// <summary>
/// Gets or sets the stickers sent with this message.
/// </summary>
@@ -100,14 +90,32 @@ namespace Discord
/// </summary>
public MessageFlags Flags { get; set; }

/// <summary>
/// Creates a new <see cref="MessageBuilder"/> based on the value of <paramref name="content"/>.
/// </summary>
/// <param name="content">The message content to create this <see cref="MessageBuilder"/> from.</param>
public MessageBuilder(string content)
{
Content = new TextBuilder(content);
}

public MessageBuilder()
{
_embeds = new();
_stickers = new();
_files = new();

Components = new();
}

/// <summary>
/// Sets the <see cref="Content"/> of this message.
/// </summary>
/// <param name="content">The content of the message.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithContent(string content)
public virtual MessageBuilder WithContent(TextBuilder builder)
{
Content = content;
Content = builder;
return this;
}

@@ -116,7 +124,7 @@ namespace Discord
/// </summary>
/// <param name="isTTS">whether or not this message is tts.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithTTS(bool isTTS)
public virtual MessageBuilder WithTTS(bool isTTS)
{
IsTTS = isTTS;
return this;
@@ -128,7 +136,7 @@ namespace Discord
/// <param name="embeds">The embeds to be put in this message.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentOutOfRangeException">A message can only contain a maximum of <see cref="DiscordConfig.MaxEmbedsPerMessage"/> embeds.</exception>
public MessageBuilder WithEmbeds(params EmbedBuilder[] embeds)
public virtual MessageBuilder WithEmbeds(params EmbedBuilder[] embeds)
{
Embeds = new(embeds);
return this;
@@ -140,7 +148,7 @@ namespace Discord
/// <param name="embed">The embed builder to add</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentOutOfRangeException">A message can only contain a maximum of <see cref="DiscordConfig.MaxEmbedsPerMessage"/> embeds.</exception>
public MessageBuilder AddEmbed(EmbedBuilder embed)
public virtual MessageBuilder AddEmbed(EmbedBuilder embed)
{
if (_embeds?.Count >= DiscordConfig.MaxEmbedsPerMessage)
throw new ArgumentOutOfRangeException(nameof(embed.Length), $"A message can only contain a maximum of {DiscordConfig.MaxEmbedsPerMessage} embeds");
@@ -157,7 +165,7 @@ namespace Discord
/// </summary>
/// <param name="allowedMentions">The allowed mentions for this message.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithAllowedMentions(AllowedMentions allowedMentions)
public virtual MessageBuilder WithAllowedMentions(AllowedMentions allowedMentions)
{
AllowedMentions = allowedMentions;
return this;
@@ -168,7 +176,7 @@ namespace Discord
/// </summary>
/// <param name="reference">The message reference (reply-to) for this message.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithMessageReference(MessageReference reference)
public virtual MessageBuilder WithMessageReference(MessageReference reference)
{
MessageReference = reference;
return this;
@@ -179,7 +187,7 @@ namespace Discord
/// </summary>
/// <param name="message">The message to set as a reference.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithMessageReference(IMessage message)
public virtual MessageBuilder WithMessageReference(IMessage message)
{
if (message != null)
MessageReference = new MessageReference(message.Id, message.Channel?.Id, ((IGuildChannel)message.Channel)?.GuildId);
@@ -191,7 +199,7 @@ namespace Discord
/// </summary>
/// <param name="builder">The component builder to set.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithComponentBuilder(ComponentBuilder builder)
public virtual MessageBuilder WithComponentBuilder(ComponentBuilder builder)
{
Components = builder;
return this;
@@ -203,7 +211,7 @@ namespace Discord
/// <param name="button">The button builder to add.</param>
/// <param name="row">The optional row to place the button on.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithButton(ButtonBuilder button, int row = 0)
public virtual MessageBuilder WithButton(ButtonBuilder button, int row = 0)
{
Components ??= new();
Components.WithButton(button, row);
@@ -221,7 +229,7 @@ namespace Discord
/// <param name="disabled">Whether or not the newly created button is disabled.</param>
/// <param name="row">The row the button should be placed on.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithButton(
public virtual MessageBuilder WithButton(
string label = null,
string customId = null,
ButtonStyle style = ButtonStyle.Primary,
@@ -241,7 +249,7 @@ namespace Discord
/// <param name="menu">The select menu builder to add.</param>
/// <param name="row">The optional row to place the select menu on.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0)
public virtual MessageBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0)
{
Components ??= new();
Components.WithSelectMenu(menu, row);
@@ -259,7 +267,7 @@ namespace Discord
/// <param name="disabled">Whether or not the menu is disabled.</param>
/// <param name="row">The row to add the menu to.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options,
public virtual MessageBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options,
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0)
{
Components ??= new();
@@ -272,7 +280,7 @@ namespace Discord
/// </summary>
/// <param name="files">The file collection to set.</param>
/// <returns>The current builder.</returns>
public MessageBuilder WithFiles(IEnumerable<FileAttachment> files)
public virtual MessageBuilder WithFiles(IEnumerable<FileAttachment> files)
{
Files = new List<FileAttachment>(files);
return this;
@@ -283,7 +291,7 @@ namespace Discord
/// </summary>
/// <param name="file">The file to add.</param>
/// <returns>The current builder.</returns>
public MessageBuilder AddFile(FileAttachment file)
public virtual MessageBuilder AddFile(FileAttachment file)
{
Files.Add(file);
return this;
@@ -300,7 +308,7 @@ namespace Discord
: ImmutableArray<Embed>.Empty;

return new Message(
_content,
Content.Build(),
IsTTS,
embeds,
AllowedMentions,

+ 125
- 0
src/Discord.Net.Core/Entities/Messages/Builders/MultiLineBuilder.cs View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a builder for multi-line text.
/// </summary>
public class MultiLineBuilder
{
/// <summary>
/// The underlying list of lines this builder uses to construct multiline text.
/// </summary>
public List<string> Lines { get; set; }

/// <summary>
/// Creates a new instance of <see cref="MultiLineBuilder"/>.
/// </summary>
public MultiLineBuilder()
{
Lines = new();
}

/// <summary>
/// Creates a new instance of <see cref="MultiLineBuilder"/> with a pre-defined capacity.
/// </summary>
/// <param name="capacity"></param>
public MultiLineBuilder(int capacity)
{
Lines = new(capacity);
}

/// <summary>
/// Creates a new instance of <see cref="MultiLineBuilder"/> with a number of lines pre-defined.
/// </summary>
/// <param name="entries">The range of lines to add to this builder.</param>
public MultiLineBuilder(params string[] entries)
{
Lines = new(entries);
}

/// <summary>
/// Adds a line to the builder.
/// </summary>
/// <param name="text">The text to add to this line.</param>
/// <returns>The same instance with a line appended.</returns>
public MultiLineBuilder AddLine(string text)
{
Lines.Add(text);
return this;
}

/// <summary>
/// Adds a range of lines to the builder.
/// </summary>
/// <param name="text">The range of text to add.</param>
/// <returns>The same instance with a range of lines appended.</returns>
public MultiLineBuilder AddLines(IEnumerable<string> text)
{
if (!text.Any())
throw new ArgumentException("The passed range does not contain any values", nameof(text));

Lines.AddRange(text);
return this;
}

/// <summary>
/// Removes a (or more) line(s) from the builder.
/// </summary>
/// <param name="predicate">The predicate to remove lines with.</param>
/// <returns>The same instance with all lines matching <paramref name="predicate"/> removed.</returns>
public MultiLineBuilder RemoveLine(Predicate<string> predicate)
{
if (predicate is null)
throw new ArgumentNullException(nameof(predicate));

Lines.RemoveAll(x => predicate(x));
return this;
}

/// <summary>
/// Removes a line from the builder.
/// </summary>
/// <param name="index">The index to remove a line at.</param>
/// <returns></returns>
public MultiLineBuilder RemoveLine(int index)
{
Lines.RemoveAt(index);
return this;
}

/// <summary>
/// Gets the line at a specific index.
/// </summary>
/// <param name="index">The index to get a line for.</param>
/// <returns>The line at defined <paramref name="index"/>.</returns>
public string this[int index]
{
get
{
return Lines[index];
}
}

/// <summary>
/// Builds the builder into multiline text.
/// </summary>
/// <returns>A string representing the lines added in this builder.</returns>
public string Build()
=> string.Join(Environment.NewLine, Lines);

/// <summary>
/// Creates a string from the lines currently present in <see cref="Lines"/>.
/// </summary>
/// <remarks>
/// This method has the same behavior as <see cref="Build"/>.
/// </remarks>
/// <returns>A string representing the lines added in this builder.</returns>
public override string ToString()
=> string.Join(Environment.NewLine, Lines);
}
}

+ 493
- 0
src/Discord.Net.Core/Entities/Messages/Builders/TextBuilder.cs View File

@@ -0,0 +1,493 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a builder to build Discord messages with markdown with.
/// </summary>
public class TextBuilder
{
private readonly StringBuilder _builder;

private bool _lineStart = false;

/// <summary>
/// Creates a new instance of <see cref="TextBuilder"/>.
/// </summary>
public TextBuilder()
{
_builder = new();
}

/// <summary>
/// Creates a new instance of <see cref="TextBuilder"/> with a starting string appended.
/// </summary>
/// <param name="startingString">The string to start the builder with.</param>
public TextBuilder(string startingString)
{
_builder = new(startingString);
}

/// <summary>
/// Creates a new instance of <see cref="TextBuilder"/> with a capacity and (optionally) max capacity defined.
/// </summary>
/// <param name="capacity">The init capacity of the underlying <see cref="StringBuilder"/>.</param>
/// <param name="maxCapacity">The maximum capacity of the underlying <see cref="StringBuilder"/>.</param>
public TextBuilder(int capacity, int? maxCapacity = null)
{
if (maxCapacity is not null)
_builder = new(capacity, maxCapacity.Value);
else
_builder = new(capacity);
}

/// <summary>
/// Adds a header to the builder.
/// </summary>
/// <remarks>
/// [Note] Headers are only supported in forums, which are not released publically yet.
/// </remarks>
/// <param name="text">The text to be present in the header.</param>
/// <param name="format">The header format.</param>
/// <param name="skipLine">If the builder should skip a line when creating the next parameter.</param>
/// <returns>The same instance with a header appended. This method will append a new line below the header.</returns>
public TextBuilder AddHeader(string text, HeaderFormat format, bool skipLine = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

if (skipLine)
_builder.AppendLine();
_builder.AppendLine(text.ToHeader(format));
_lineStart = true;
return this;
}

/// <summary>
/// Adds bold text to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with bold text appended.</returns>
public TextBuilder AddBoldText(string text, bool inline = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));
Construct(Format.Bold(text), inline);
return this;
}

/// <summary>
/// Adds bold text to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with bold text appended.</returns>
public TextBuilder AddBoldText(MultiLineBuilder builder, bool inline = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddBoldText(text, inline);
}

/// <summary>
/// Adds italic text to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with italic appended.</returns>
public TextBuilder AddItalicText(string text, bool inline = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

Construct(Discord.Format.Italics(text), inline);
return this;
}

/// <summary>
/// Adds italic text to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with italic text appended.</returns>
public TextBuilder AddItalicText(MultiLineBuilder builder, bool inline = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddItalicText(text, inline);
}

/// <summary>
/// Adds plain text to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with plain text appended.</returns>
public TextBuilder AddPlainText(string text, bool inline = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

Construct(text, inline);
return this;
}

/// <summary>
/// Adds plain text to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with plain text appended.</returns>
public TextBuilder AddPlainText(MultiLineBuilder builder, bool inline = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddPlainText(text, inline);
}

/// <summary>
/// Adds underlined text to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with underlined text appended.</returns>
public TextBuilder AddUnderlinedText(string text, bool inline = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

Construct(text.ToUnderline(), inline);
return this;
}

/// <summary>
/// Adds underlined text to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with underlined text appended.</returns>
public TextBuilder AddUnderlinedText(MultiLineBuilder builder, bool inline = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddUnderlinedText(text, inline);
}

/// <summary>
/// Adds a timestamp to the builder.
/// </summary>
/// <param name="dateTime">The time for which this timestamp should be created.</param>
/// <param name="style">The style of the stamp.</param>
/// <param name="inline">If the stamp should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with a timestamp appended.</returns>
public TextBuilder AddTimestamp(DateTime dateTime, TimestampTagStyles style, bool inline = true)
{
Construct(TimestampTag.FromDateTime(dateTime, style).ToString(), inline);
return this;
}

/// <summary>
/// Adds strikethrough text to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with striked through text appended.</returns>
public TextBuilder AddStrikeThroughText(string text, bool inline = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

Construct(text.ToStrikethrough(), inline);
return this;
}

/// <summary>
/// Adds strikethrough text to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with striked through text appended.</returns>
public TextBuilder AddStrikeThroughText(MultiLineBuilder builder, bool inline = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddStrikeThroughText(text, inline);
}

/// <summary>
/// Adds a spoiler to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with a spoiler appended.</returns>
public TextBuilder AddSpoiler(string text, bool inline = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

Construct(text.ToSpoiler(), inline);
return this;
}

/// <summary>
/// Adds a spoiler to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with a spoiler appended.</returns>
public TextBuilder AddSpoiler(MultiLineBuilder builder, bool inline = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddSpoiler(text, inline);
}

/// <summary>
/// Adds a quote to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="skipLine">If the builder should skip a line when creating the next parameter.</param>
/// <returns>The same instance with a quote appended. This method will append a new line below the quote.</returns>
public TextBuilder AddQuote(string text, bool skipLine = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

if (skipLine)
_builder.AppendLine();
_builder.AppendLine(text.ToQuote());
_lineStart = true;
return this;
}

/// <summary>
/// Adds a quote to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with a quote appended.</returns>
public TextBuilder AddQuote(MultiLineBuilder builder, bool inline = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddQuote(text, inline);
}

/// <summary>
/// Adds a block quote to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="skipLine">If the builder should skip a line when creating the next parameter.</param>
/// <returns>The same instance with a block quote appended. This method will append a new line below the quote.</returns>
public TextBuilder AddBlockQuote(string text, bool skipLine = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

if (skipLine)
_builder.AppendLine();
_builder.AppendLine(text.ToBlockQuote());
_lineStart = true;
return this;
}

/// <summary>
/// Adds a block quote to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="skipLine">If the builder should skip a line when creating the next parameter.</param>
/// <returns>The same instance with a block quote appended. This method will append a new line below the quote.</returns>
public TextBuilder AddBlockQuote(MultiLineBuilder builder, bool skipLine = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddBlockQuote(text, skipLine);
}

/// <summary>
/// Adds code marked text to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with code marked text appended.</returns>
public TextBuilder AddCode(string text, bool inline = false)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

Construct(text.ToCode(), inline);
return this;
}

/// <summary>
/// Adds code marked text to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="inline">If the text should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with code marked text appended.</returns>
public TextBuilder AddCode(MultiLineBuilder builder, bool inline = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddCode(text, inline);
}

/// <summary>
/// Adds a code block to the builder.
/// </summary>
/// <param name="text">The text to be present in the markdown.</param>
/// <param name="lang">The language in which this code should be presented.</param>
/// <param name="skipLine">If the builder should skip a line when creating the next parameter.</param>
/// <returns>The same instance with a code block appended. This method will append a new line below the block.</returns>
public TextBuilder AddCodeBlock(string text, CodeLanguage? lang = null, bool skipLine = true)
{
if (string.IsNullOrEmpty(text))
throw new ArgumentException("Value cannot be null or empty.", nameof(text));

lang ??= CodeLanguage.None;
if (skipLine)
_builder.AppendLine();
_builder.AppendLine(text.ToCodeBlock(lang));
_lineStart = true;
return this;
}

/// <summary>
/// Adds a code block to the builder.
/// </summary>
/// <param name="builder">A builder for multiline text.</param>
/// <param name="lang">The language in which this code should be presented.</param>
/// <param name="skipLine">If the builder should skip a line when creating the next parameter.</param>
/// <returns>The same instance with a code block appended. This method will append a new line below the quote.</returns>
public TextBuilder AddCodeBlock(MultiLineBuilder builder, CodeLanguage? lang = null, bool skipLine = true)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));

var text = builder.Build();
return AddCodeBlock(text, lang, skipLine);
}

/// <summary>
/// Adds an emote to the builder.
/// </summary>
/// <param name="emote">The emote to add.</param>
/// <param name="inline">If the emote should be appended in the same line or if it should append to a new line.</param>
/// <returns>The same instance with an emote appended.</returns>
public TextBuilder AddEmote(IEmote emote, bool inline = false)
{
if (emote is null)
throw new ArgumentNullException(nameof(emote));
var str = emote switch
{
Emote ee => ee.ToString(),
Emoji ei => ei.ToString(),
_ => null
};

Construct(str, inline);
return this;
}

/// <summary>
/// Adds a range of emotes to the builder.
/// </summary>
/// <param name="seperator">The seperator to join the emotes with.</param>
/// <param name="inline">If the emotes should be appended in the same line or if it should append to a new line.</param>
/// <param name="emotes">The range of emotes to add.</param>
/// <returns>The same instance with a range of emotes appended.</returns>
public TextBuilder AddEmotes(string seperator, bool inline = false, params IEmote[] emotes)
{
if (!emotes.Any())
throw new ArgumentException("No values were found in the passed selection", nameof(emotes));

var str = string.Join(seperator, emotes.Select(x =>
{
return x switch
{
Emote emote => emote.ToString(),
Emoji emoji => emoji.ToString(),
_ => throw new ArgumentNullException(nameof(emotes)),
};
}));
Construct(str, inline);
return this;
}

/// <summary>
/// Starts the next query to the builder on a new line.
/// </summary>
/// <returns>The same instance with an empty line appended.</returns>
public TextBuilder AddNewline()
{
_builder.AppendLine();
_lineStart = true;
return this;
}

/// <summary>
/// Builds a Discord message string from this instance.
/// </summary>
/// <returns>The string to send to Discord.</returns>
public string Build()
=> _builder.ToString();

private void Construct(string text, bool inline)
{
if (_builder.Length + text.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Maximum message length of {DiscordConfig.MaxMessageSize} has been reached.");

if (inline)
{
if (!_lineStart)
text = " " + text;

else
_lineStart = false;

_builder.Append(text); // add a space to define
}
else
{
if (_lineStart)
_lineStart = false;
_builder.AppendLine();
_builder.Append(text);
}
}

/// <summary>
/// Builds the underlying <see cref="StringBuilder"/> to a string.
/// </summary>
/// <remarks>
/// This method has the same functionality as <see cref="Build"/>.
/// </remarks>
/// <returns>The string to send to Discord.</returns>
public override string ToString()
=> _builder.ToString();
}
}

+ 101
- 0
src/Discord.Net.Core/Extensions/MarkdownExtensions.cs View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
internal static class MarkdownExtensions
{
public static string ToBold(this string text, int index = 0, int? count = null) //=> $"**{text}**";
{
var length = count ?? (text.Length - index);

return text.Format($"**{text.Substring(index, (index + length))}**", index, length);
}

public static string ToItalic(this string text, int index = 0, int? count = null) //=> $"*{text}*";
{
var length = count ?? (text.Length - index);

return text.Format($"*{text.Substring(index, (index + length))}*", index, length);
}

public static string ToUnderline(this string text, int index = 0, int? count = null) //=> $"__{text}__";
{
var length = count ?? (text.Length - index);

return text.Format($"__{text.Substring(index, (index + length))}__", index, length);
}

public static string ToStrikethrough(this string text, int index = 0, int? count = null) //=> $"~~{text}~~";
{
var length = count ?? (text.Length - index);

return text.Format($"~~{text.Substring(index, (index + length))}~~", index, length);
}

public static string ToSpoiler(this string text, int index = 0, int? count = null) //=> $"||{text}||";
{
var length = count ?? (text.Length - index);

return text.Format($"||{text.Substring(index, (index + length))}||", index, length);
}

public static string ToQuote(this string text, int index = 0, int? count = null) //=> $"> {text}";
{
if (index is 0 && count is null)
text = text.Replace(Environment.NewLine, $"{Environment.NewLine}> ");

var length = count ?? (text.Length - index);

return text.Format($"{Environment.NewLine}> {text.Substring(index, (index + length))}{Environment.NewLine}", index, length);
}

public static string ToBlockQuote(this string text, int index = 0, int? count = null) //=> $">>> {text}";
{
var length = count ?? (text.Length - index);

return text.Format($"{Environment.NewLine}>>> {text.Substring(index, (index + length))}{Environment.NewLine}", index, length);
}

public static string ToCode(this string text, int index = 0, int? count = null) //=> $"`{text}`";
{
var length = count ?? (text.Length - index);

return text.Format($"`{text.Substring(index, (index + length))}`", index, length);
}

public static string ToCodeBlock(this string text, CodeLanguage? lang = null, int index = 0, int? count = null)
{
lang ??= CodeLanguage.None;

var length = count ?? (text.Length - index);

return text.Format($"```{lang.Value}{Environment.NewLine}{text.Substring(index, (index + length))}{Environment.NewLine}```", index, length);
}

public static string ToHyperLink(this string text, string url, int index = 0, int? count = null)
{
var length = count ?? (text.Length - index);

return text.Format($"[{text.Substring(index, (index + length))}]({url})", index, length);
}

public static string ToHeader(this string text, HeaderFormat format, int index = 0, int? count = null)
{
var length = count ?? (text.Length - index);

return text.Format($"{Environment.NewLine}{format.Format} {text.Substring(index, (index + length))} {Environment.NewLine}", index, length);
}

public static string WithTimestamp(this string text, DateTime dateTime, TimestampTagStyles style, int index = 0)
=> text.Insert(index, TimestampTag.FromDateTime(dateTime, style).ToString());

private static string Format(this string text, string format, int index, int length)
{
return text.Insert(index, format).Remove(index + format.Length, length);
}
}
}

+ 33
- 9
src/Discord.Net.Core/Format.cs View File

@@ -10,22 +10,44 @@ namespace Discord
private static readonly string[] SensitiveCharacters = {
"\\", "*", "_", "~", "`", ".", ":", "/", ">", "|" };

/// <summary> Returns a markdown-formatted string with bold formatting. </summary>
/// <summary>
/// Returns a markdown-formatted string with bold formatting.
/// </summary>
public static string Bold(string text) => $"**{text}**";
/// <summary> Returns a markdown-formatted string with italics formatting. </summary>

/// <summary>
/// Returns a markdown-formatted string with italics formatting.
/// </summary>
public static string Italics(string text) => $"*{text}*";
/// <summary> Returns a markdown-formatted string with underline formatting. </summary>

/// <summary>
/// Returns a markdown-formatted string with underline formatting.
/// </summary>
public static string Underline(string text) => $"__{text}__";
/// <summary> Returns a markdown-formatted string with strike-through formatting. </summary>

/// <summary>
/// Returns a markdown-formatted string with strike-through formatting.
/// </summary>
public static string Strikethrough(string text) => $"~~{text}~~";
/// <summary> Returns a string with spoiler formatting. </summary>

/// <summary>
/// Returns a string with spoiler formatting.
/// </summary>
public static string Spoiler(string text) => $"||{text}||";
/// <summary> Returns a markdown-formatted URL. Only works in <see cref="EmbedBuilder"/> descriptions and fields. </summary>

/// <summary>
/// Returns a markdown-formatted URL. Only works in <see cref="EmbedBuilder"/> descriptions and fields.
/// </summary>
public static string Url(string text, string url) => $"[{text}]({url})";
/// <summary> Escapes a URL so that a preview is not generated. </summary>

/// <summary>
/// Escapes a URL so that a preview is not generated.
/// </summary>
public static string EscapeUrl(string url) => $"<{url}>";

/// <summary> Returns a markdown-formatted string with codeblock formatting. </summary>
/// <summary>
/// Returns a markdown-formatted string with codeblock formatting.
/// </summary>
public static string Code(string text, string language = null)
{
if (language != null || text.Contains("\n"))
@@ -34,7 +56,9 @@ namespace Discord
return $"`{text}`";
}

/// <summary> Sanitizes the string, safely escaping any Markdown sequences. </summary>
/// <summary>
/// Sanitizes the string, safely escaping any Markdown sequences.
/// </summary>
public static string Sanitize(string text)
{
foreach (string unsafeChar in SensitiveCharacters)


Loading…
Cancel
Save