| @@ -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}"; | |||
| } | |||
| } | |||
| @@ -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}"; | |||
| } | |||
| } | |||
| @@ -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, | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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) | |||