diff --git a/.markdownlint.json b/.markdownlint.json
index 25e746e53..a94809e1d 100644
--- a/.markdownlint.json
+++ b/.markdownlint.json
@@ -4,7 +4,8 @@
"allowed_elements": [
"details",
"summary",
- "span"
+ "span",
+ "br"
]
}
}
diff --git a/GW2SDK/Features/Markup/MarkupColorName.cs b/GW2SDK/Features/Markup/MarkupColorName.cs
index 25ae64607..92650c474 100644
--- a/GW2SDK/Features/Markup/MarkupColorName.cs
+++ b/GW2SDK/Features/Markup/MarkupColorName.cs
@@ -1,4 +1,4 @@
-using static System.StringComparison;
+using System.Collections.ObjectModel;
namespace GuildWars2.Markup;
@@ -6,31 +6,50 @@ namespace GuildWars2.Markup;
[PublicAPI]
public static class MarkupColorName
{
- /// The color for flavor text.
+ /// The color for flavor text. Color used in game: Aqua.
public static string Flavor => "@flavor";
- /// The color for reminder text.
+ /// The color for reminder text. Color used in game: Gray.
public static string Reminder => "@reminder";
- /// The color for ability type text.
+ /// The color for ability type text. Color used in game: Light Yellow.
public static string AbilityType => "@abilitytype";
- /// The color for warning text.
+ /// The color for warning text. Color used in game: Red.
public static string Warning => "@warning";
- /// The color for task text.
+ /// The color for task text. Color used in game: Gold.
public static string Task => "@task";
+ ///
+ /// A dictionary that maps color names to their corresponding hex color codes, based on colors picked from the game.
+ ///
+ ///
+ /// The dictionary is case-insensitive and contains the following default mappings:
+ ///
+ /// - @abilitytype#ffee88 (light yellow)
+ /// - @flavor#99dddd (aqua)
+ /// - @reminder#aaaaaa (gray)
+ /// - @task#ffcc55 (gold)
+ /// - @warning#ff0000 (red)
+ ///
+ ///
+ public static readonly IReadOnlyDictionary DefaultColorMap =
+ new ReadOnlyDictionary(new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ [AbilityType] = "#ffee88",
+ [Flavor] = "#99dddd",
+ [Reminder] = "#aaaaaa",
+ [Task] = "#ffcc55",
+ [Warning] = "#ff0000"
+ });
+
/// Determines whether the specified color name is defined.
/// The name should include the leading '@' symbol, e.g. '@flavor'.
/// The name of the color to check.
/// true if the specified color name is defined; otherwise, false.
public static bool IsDefined(string colorName)
{
- return string.Equals(colorName, Flavor, OrdinalIgnoreCase)
- || string.Equals(colorName, Reminder, OrdinalIgnoreCase)
- || string.Equals(colorName, AbilityType, OrdinalIgnoreCase)
- || string.Equals(colorName, Warning, OrdinalIgnoreCase)
- || string.Equals(colorName, Task, OrdinalIgnoreCase);
+ return DefaultColorMap.ContainsKey(colorName);
}
}
diff --git a/GW2SDK/Features/Markup/MarkupConverter.cs b/GW2SDK/Features/Markup/MarkupConverter.cs
index bac055370..b6e367e84 100644
--- a/GW2SDK/Features/Markup/MarkupConverter.cs
+++ b/GW2SDK/Features/Markup/MarkupConverter.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+
namespace GuildWars2.Markup;
/// Provides functionality to convert markup strings to other formats.
@@ -27,7 +29,7 @@ public static string ToPlainText(string markup)
return TextConverter.Convert(rootNode);
}
- /// Converts a markup string to a string with HTML formatting.
+ /// Converts a markup string to a string with HTML formatting using the .
/// The markup string to convert.
/// The HTML string.
public static string ToHtml(string markup)
@@ -42,4 +44,19 @@ public static string ToHtml(string markup)
return HtmlConverter.Convert(rootNode);
}
+ /// Converts a markup string to a string with HTML formatting using a custom color map.
+ /// The markup string to convert.
+ /// A dictionary mapping color names to their corresponding HTML color codes.
+ /// The HTML string.
+ public static string ToHtml(string markup, IReadOnlyDictionary colorMap)
+ {
+ if (string.IsNullOrWhiteSpace(markup))
+ {
+ return markup;
+ }
+
+ var tokens = Lexer.Tokenize(markup);
+ var rootNode = Parser.Parse(tokens);
+ return HtmlConverter.Convert(rootNode, colorMap);
+ }
}
diff --git a/GW2SDK/Features/Markup/MarkupHtmlConverter.cs b/GW2SDK/Features/Markup/MarkupHtmlConverter.cs
index c66f0516f..290f0497d 100644
--- a/GW2SDK/Features/Markup/MarkupHtmlConverter.cs
+++ b/GW2SDK/Features/Markup/MarkupHtmlConverter.cs
@@ -9,22 +9,41 @@ namespace GuildWars2.Markup;
public sealed class MarkupHtmlConverter
{
///
- /// Converts a to its HTML representation.
+ /// Converts a to its HTML representation using the .
///
- /// The root node containing nodes to be converted.
- /// A string representation of the nodes within the root node.
+ /// The root node of the markup syntax tree to convert.
+ /// A string containing the HTML representation of the markup syntax tree.
public string Convert(RootNode root)
{
+ return Convert(root, MarkupColorName.DefaultColorMap);
+ }
+
+ /// Converts a and its children to an HTML string representation using a custom color map.
+ /// The root node of the markup syntax tree to convert.
+ /// A dictionary mapping color names to their corresponding HTML color codes.
+ /// A string containing the HTML representation of the markup syntax tree.
+ public string Convert(RootNode root, IReadOnlyDictionary? colorMap)
+ {
+ if (colorMap == null)
+ {
+ colorMap = MarkupColorName.DefaultColorMap;
+ }
+ else if (colorMap != MarkupColorName.DefaultColorMap)
+ {
+ // Ensure the key comparison is case-insensitive
+ colorMap = colorMap.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
+ }
+
var builder = new StringBuilder();
foreach (var node in root.Children)
{
- builder.Append(ConvertNode(node));
+ builder.Append(ConvertNode(node, colorMap));
}
return builder.ToString();
}
- private string ConvertNode(MarkupNode node)
+ private string ConvertNode(MarkupNode node, IReadOnlyDictionary colorMap)
{
switch (node)
{
@@ -33,30 +52,20 @@ private string ConvertNode(MarkupNode node)
case LineBreakNode:
return "
";
case ColoredTextNode coloredText:
- var content = string.Concat(coloredText.Children.Select(ConvertNode));
- if (coloredText.Color.StartsWith("#"))
+ var builder = new StringBuilder();
+ foreach (var child in coloredText.Children)
{
- return $"{content}";
+ builder.Append(ConvertNode(child, colorMap));
}
- else if (string.Equals(coloredText.Color, MarkupColorName.Flavor, StringComparison.OrdinalIgnoreCase))
- {
- return $"{content}";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.Reminder, StringComparison.OrdinalIgnoreCase))
- {
- return $"{content}";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.AbilityType, StringComparison.OrdinalIgnoreCase))
- {
- return $"{content}";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.Warning, StringComparison.OrdinalIgnoreCase))
+
+ var content = builder.ToString();
+ if (coloredText.Color.StartsWith("#"))
{
- return $"{content}";
+ return $"{content}";
}
- else if (string.Equals(coloredText.Color, MarkupColorName.Task, StringComparison.OrdinalIgnoreCase))
+ else if (colorMap.TryGetValue(coloredText.Color, out var color))
{
- return $"{content}";
+ return $"{content}";
}
else
{
diff --git a/docs/guide/getting-started/formatted-text.md b/docs/guide/getting-started/formatted-text.md
index 3f2046b46..ab27fd4a8 100644
--- a/docs/guide/getting-started/formatted-text.md
+++ b/docs/guide/getting-started/formatted-text.md
@@ -15,8 +15,8 @@ Double-click to apply to an unused infusion slot. Adds a festive glow.
Which is rendered as:
> Double-click to apply to an unused infusion slot. Adds a festive glow.
-> Warning!
-> Captain's Council recommends avoiding direct
+> Warning!
+> Captain's Council recommends avoiding direct
contact with this substance.
## Converting formatted text to other formats
@@ -30,59 +30,76 @@ it to other formats:
### Converting formatted text to plain text
```csharp
-var lexer = new MarkupLexer();
-var parser = new MarkupParser();
-var converter = new MarkupTextConverter();
+using GuildWars2.Markup;
var input = "Double-click to apply to an unused infusion slot. Adds a festive glow."
+ "\nWarning!"
+ "\nCaptain's Council recommends avoiding direct contact with this"
+ " substance.";
-var tokens = lexer.Tokenize(input);
-var syntax = parser.Parse(tokens);
-var actual = converter.Convert(syntax);
-Console.WriteLine(actual);
+
+var plainText = MarkupConverter.ToPlainText(input);
+Console.WriteLine(plainText);
```
Output:
```text
-Double-click to apply to an unused infusion slot. Adds a festive glow
+Double-click to apply to an unused infusion slot. Adds a festive glow.
Warning!
-Captain's Council recommends avoiding direct contact with this substance.
+Captain's Council recommends avoiding direct contact with this substance
```
### Converting formatted text to HTML
```csharp
-var lexer = new MarkupLexer();
-var parser = new MarkupParser();
-var converter = new MarkupHtmlConverter();
-
var input = "Double-click to apply to an unused infusion slot. Adds a festive glow."
+ "\nWarning!"
+ "\nCaptain's Council recommends avoiding direct contact with this"
+ " substance.";
-var tokens = lexer.Tokenize(input);
-var syntax = parser.Parse(tokens);
-var actual = converter.Convert(syntax);
-Console.WriteLine(actual);
+
+var html = MarkupConverter.ToHtml(input);
+Console.WriteLine(html);
```
Output:
```html
Double-click to apply to an unused infusion slot. Adds a festive glow.
-Warning!
Captain's
+Warning!
Captain's
Council recommends avoiding direct contact with this substance.
```
+#### Overriding default colors
+
+Optionally, you can override the default colors. Start by cloning the default
+color map, and then modify the values:
+
+```csharp
+var input = "Double-click to apply to an unused infusion slot. Adds a festive glow."
+ + "\nWarning!"
+ + "\nCaptain's Council recommends avoiding direct contact with this"
+ + " substance.";
+
+var colorMap = new Dictionary(MarkupColorName.DefaultColorMap)
+{
+ [MarkupColorName.Flavor] = "hotpink"
+};
+
+var html = MarkupConverter.ToHtml(input, colorMap);
+Console.WriteLine(html);
+```
+
+Output (rendered in HTML):
+
+> Double-click to apply to an unused infusion slot. Adds a festive glow.
+Warning!
+Captain's Council recommends avoiding direct contact with this substance.
+
### Building a custom formatter
-Depending on your UI framework, you may want to build a custom formatter to convert
-the game's markup language to your UI framework's text formatting. You can use the
-`MarkupLexer` and `MarkupParser` to tokenize and parse the markup language, and then
-convert the syntax tree to your desired format.
+You may want to build a custom formatter for your UI framework if it can't render
+HTML. You can use the `MarkupLexer` and `MarkupParser` to tokenize and parse the
+markup language, and then convert the syntax tree to the desired format.
For example, to convert the markup language to the markup language used by
[Spectre.Console](https://spectreconsole.net/markup):
@@ -118,62 +135,83 @@ public class SpectreMarkupConverter
private string ConvertNode(MarkupNode node)
{
// MarkupNode is the base type for all nodes in the syntax tree.
- // Use pattern matching to convert each type of node to the desired format.
- switch (node)
+ // Use a switch statement to convert each type of node to the desired format.
+ // You could also use pattern matching with C# 9.
+ switch (node.Type)
{
// TextNode is just a plain text node, no formatting.
- case TextNode text:
+ case MarkupNodeType.Text:
+ var text = (TextNode)node;
return Markup.Escape(text.Text);
// LineBreakNode represents a line break, covers both \n and
- case LineBreakNode:
+ case MarkupNodeType.LineBreak:
return Environment.NewLine;
- // ColoredTextNode represents text with a color like text
- // or text
- case ColoredTextNode coloredText:
- var content = string.Concat(coloredText.Children.Select(ConvertNode));
- if (coloredText.Color.StartsWith("#", StringComparison.Ordinal))
- {
- return $"[${coloredText.Color}]{content}[/]";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.Flavor,
- StringComparison.OrdinalIgnoreCase))
- {
- return $"[#9BE8E4]{content}[/]";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.Reminder,
- StringComparison.OrdinalIgnoreCase))
+ // ColoredTextNode represents text with a color like text
+ // or text
+ case MarkupNodeType.ColoredText:
+ var coloredText = (ColoredTextNode)node;
+ var builder = new StringBuilder();
+ foreach (var child in coloredText.Children)
{
- return $"[#B0B0B0]{content}[/]";
+ builder.Append(ConvertNode(child));
}
- else if (string.Equals(coloredText.Color, MarkupColorName.AbilityType,
- StringComparison.OrdinalIgnoreCase))
- {
- return $"[#FFEC8C]{content}[/]";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.Warning,
- StringComparison.OrdinalIgnoreCase))
+
+ var content = builder.ToString();
+ if (coloredText.Color.StartsWith("#", StringComparison.Ordinal))
{
- return $"[#ED0002]{content}[/]";
+ var colorCode = coloredText.Color;
+ return $"[{colorCode}]{content}[/]";
}
- else if (string.Equals(coloredText.Color, MarkupColorName.Task,
- StringComparison.OrdinalIgnoreCase))
+ else if (ColorMap.TryGetValue(coloredText.Color, out var colorCode))
{
- return $"[#FFC957]{content}[/]";
+ return $"[{colorCode}]{content}[/]";
}
else
{
return content;
}
+
default:
return "";
}
}
+
+ // A map of color names to hexadecimal RGB values.
+ // Note that the color names are case-insensitive.
+ private static readonly IReadOnlyDictionary ColorMap
+ = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ [MarkupColorName.Flavor] = "#99dddd",
+ [MarkupColorName.Reminder] = "#aaaaaa",
+ [MarkupColorName.AbilityType] = "#ffee88",
+ [MarkupColorName.Warning] = "#ff0000",
+ [MarkupColorName.Task] = "#ffcc55",
+ };
}
```
+Usage:
+
+```csharp
+// Set up the lexer, parser, and converter.
+var lexer = new MarkupLexer();
+var parser = new MarkupParser();
+var converter = new SpectreMarkupConverter();
+
+// Tokenize the input
+var input = "... (markup text)";
+var tokens = lexer.Tokenize(input);
+
+// Convert the tokens to a syntax tree
+var syntax = parser.Parse(tokens);
+
+// Convert the syntax tree to the desired format
+var output = converter.Convert(syntax);
+```
+
## Language reference
The markup language is quite simple, it only supports a few tags:
@@ -181,11 +219,11 @@ The markup language is quite simple, it only supports a few tags:
- `c`: Changes the color of the text. The color is specified by a color name or
a hexadecimal RGB value.
For example, `Warning!` renders as:
- > Warning!
+ > Warning!
>
A hexadecimal RGB value can be used instead of a color name. For example,
- `Attention` renders as:
- > Attention
+ `Attention` renders as:
+ > Attention
>
- `br`: Inserts a line break. For example, `Line 1
Line 2` renders as:
> Line 1
@@ -201,7 +239,7 @@ Tags and attributes
- HTML supports tags with attributes like `text`.
- GW2 uses a simplified tag system with a single attribute name and value.
- For example, `text`.
+ For example, `text`.
Whitespace
diff --git a/samples/MostVersatileMaterials/SpectreMarkupConverter.cs b/samples/MostVersatileMaterials/SpectreMarkupConverter.cs
index 61705f597..0cfed347f 100644
--- a/samples/MostVersatileMaterials/SpectreMarkupConverter.cs
+++ b/samples/MostVersatileMaterials/SpectreMarkupConverter.cs
@@ -19,44 +19,50 @@ public string Convert(RootNode root)
private string ConvertNode(MarkupNode node)
{
- switch (node)
+ switch (node.Type)
{
- case TextNode text:
+ case MarkupNodeType.Text:
+ var text = (TextNode)node;
return Markup.Escape(text.Text);
- case LineBreakNode:
+
+ case MarkupNodeType.LineBreak:
return Environment.NewLine;
- case ColoredTextNode coloredText:
- var content = string.Concat(coloredText.Children.Select(ConvertNode));
- if (coloredText.Color.StartsWith("#", StringComparison.Ordinal))
- {
- return $"[${coloredText.Color}]{content}[/]";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.Flavor, StringComparison.OrdinalIgnoreCase))
- {
- return $"[#9BE8E4]{content}[/]";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.Reminder, StringComparison.OrdinalIgnoreCase))
- {
- return $"[#B0B0B0]{content}[/]";
- }
- else if (string.Equals(coloredText.Color, MarkupColorName.AbilityType, StringComparison.OrdinalIgnoreCase))
+
+ case MarkupNodeType.ColoredText:
+ var coloredText = (ColoredTextNode)node;
+ var builder = new StringBuilder();
+ foreach (var child in coloredText.Children)
{
- return $"[#FFEC8C]{content}[/]";
+ builder.Append(ConvertNode(child));
}
- else if (string.Equals(coloredText.Color, MarkupColorName.Warning, StringComparison.OrdinalIgnoreCase))
+
+ var content = builder.ToString();
+ if (coloredText.Color.StartsWith("#", StringComparison.Ordinal))
{
- return $"[#ED0002]{content}[/]";
+ var colorCode = coloredText.Color;
+ return $"[{colorCode}]{content}[/]";
}
- else if (string.Equals(coloredText.Color, MarkupColorName.Task, StringComparison.OrdinalIgnoreCase))
+ else if (ColorMap.TryGetValue(coloredText.Color, out var colorCode))
{
- return $"[#FFC957]{content}[/]";
+ return $"[{colorCode}]{content}[/]";
}
else
{
return content;
}
+
default:
return "";
}
}
+
+ private static readonly IReadOnlyDictionary ColorMap
+ = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ [MarkupColorName.Flavor] = "#99dddd",
+ [MarkupColorName.Reminder] = "#aaaaaa",
+ [MarkupColorName.AbilityType] = "#ffee88",
+ [MarkupColorName.Warning] = "#ff0000",
+ [MarkupColorName.Task] = "#ffcc55",
+ };
}