Skip to content

Commit 762e9c5

Browse files
authored
Add support for Slack-flavored markdown (#398)
* Add support for Slack-flavored markdown * Unit tests for Slack-flavored markdown
1 parent 3c2af15 commit 762e9c5

19 files changed

+188
-9
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*test* | *test*
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
• Item 1
2+
• Item 2
3+
• Item 3
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
_test_ | _test_
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
~test~

src/ReverseMarkdown.Test/ConverterTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,5 +1384,91 @@ public Task Bug393_RegressionWithVaryingNewLines()
13841384
var config = new Config { UnknownTags = Config.UnknownTagsOption.Bypass, ListBulletChar = '*' };
13851385
return CheckConversion(html, config);
13861386
}
1387+
1388+
[Fact]
1389+
public Task SlackFlavored_Bold()
1390+
{
1391+
const string html = "<b>test</b> | <strong>test</strong>";
1392+
var config = new Config { SlackFlavored = true };
1393+
return CheckConversion(html, config);
1394+
}
1395+
1396+
[Fact]
1397+
public Task SlackFlavored_Italic()
1398+
{
1399+
const string html = "<i>test</i> | <em>test</em>";
1400+
var config = new Config { SlackFlavored = true };
1401+
return CheckConversion(html, config);
1402+
}
1403+
1404+
[Fact]
1405+
public Task SlackFlavored_Strikethrough()
1406+
{
1407+
const string html = "<del>test</del>";
1408+
var config = new Config { SlackFlavored = true };
1409+
return CheckConversion(html, config);
1410+
}
1411+
1412+
[Fact]
1413+
public Task SlackFlavored_Bullets()
1414+
{
1415+
const string html = "<ul>\n<li>Item 1</li>\n<li>Item 2</li>\n<li>Item 3</li>\n</ul>";
1416+
var config = new Config { SlackFlavored = true };
1417+
return CheckConversion(html, config);
1418+
}
1419+
1420+
[Fact]
1421+
public void SlackFlavored_Unsupported_Hr()
1422+
{
1423+
const string html = "<hr/>";
1424+
var config = new Config { SlackFlavored = true };
1425+
var converter = new Converter(config);
1426+
Assert.Throws<SlackUnsupportedTagException>(() => converter.Convert(html));
1427+
}
1428+
1429+
[Fact]
1430+
public void SlackFlavored_Unsupported_Img()
1431+
{
1432+
const string html = "<img src=\"\"/>";
1433+
var config = new Config { SlackFlavored = true };
1434+
var converter = new Converter(config);
1435+
Assert.Throws<SlackUnsupportedTagException>(() => converter.Convert(html));
1436+
}
1437+
1438+
[Fact]
1439+
public void SlackFlavored_Unsupported_Sup()
1440+
{
1441+
const string html = "<sup>test</sup>";
1442+
var config = new Config { SlackFlavored = true };
1443+
var converter = new Converter(config);
1444+
Assert.Throws<SlackUnsupportedTagException>(() => converter.Convert(html));
1445+
}
1446+
1447+
[Fact]
1448+
public void SlackFlavored_Unsupported_Table()
1449+
{
1450+
const string html = "<table></table>";
1451+
var config = new Config { SlackFlavored = true };
1452+
var converter = new Converter(config);
1453+
Assert.Throws<SlackUnsupportedTagException>(() => converter.Convert(html));
1454+
}
1455+
1456+
[Fact]
1457+
public void SlackFlavored_Unsupported_Table_Td()
1458+
{
1459+
const string html = "<td></td>";
1460+
var config = new Config { SlackFlavored = true };
1461+
var converter = new Converter(config);
1462+
Assert.Throws<SlackUnsupportedTagException>(() => converter.Convert(html));
1463+
}
1464+
1465+
[Fact]
1466+
public void SlackFlavored_Unsupported_Table_Tr()
1467+
{
1468+
const string html = "<tr></tr>";
1469+
var config = new Config { SlackFlavored = true };
1470+
var converter = new Converter(config);
1471+
Assert.Throws<SlackUnsupportedTagException>(() => converter.Convert(html));
1472+
}
13871473
}
13881474
}

src/ReverseMarkdown/Cleaner.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ namespace ReverseMarkdown
66
{
77
public static class Cleaner
88
{
9+
private static readonly Regex SlackBoldCleaner = new Regex(@"\*(\s\*)+");
10+
private static readonly Regex SlackItalicCleaner = new Regex(@"_(\s_)+");
11+
912
private static string CleanTagBorders(string content)
1013
{
1114
// content from some htl editors such as CKEditor emits newline and tab between tags, clean that up
@@ -28,5 +31,15 @@ public static string PreTidy(string content, bool removeComments)
2831

2932
return content;
3033
}
34+
35+
public static string SlackTidy(string content)
36+
{
37+
// Slack's escaping rules depend on whether the key characters appear in
38+
// next to word characters or not.
39+
content = SlackBoldCleaner.Replace(content, "*");
40+
content = SlackItalicCleaner.Replace(content, "_");
41+
42+
return content;
43+
}
3144
}
3245
}

src/ReverseMarkdown/Config.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ public class Config
88
public UnknownTagsOption UnknownTags { get; set; } = UnknownTagsOption.PassThrough;
99

1010
public bool GithubFlavored { get; set; } = false;
11-
11+
12+
public bool SlackFlavored { get; set; } = false;
13+
1214
public bool SuppressDivNewlines { get; set; } = false;
1315

1416
public bool RemoveComments { get; set; } = false;
@@ -31,10 +33,19 @@ public class Config
3133
public TableWithoutHeaderRowHandlingOption TableWithoutHeaderRowHandling { get; set; } =
3234
TableWithoutHeaderRowHandlingOption.Default;
3335

36+
private char _listBulletChar = '-';
37+
3438
/// <summary>
3539
/// Option to set a different bullet character for un-ordered lists
3640
/// </summary>
37-
public char ListBulletChar { get; set; } = '-';
41+
/// <remarks>
42+
/// This option is ignored when <see cref="SlackFlavored"/> is enabled.
43+
/// </remarks>
44+
public char ListBulletChar
45+
{
46+
get => SlackFlavored ? '•' : _listBulletChar;
47+
set => _listBulletChar = value;
48+
}
3849

3950
/// <summary>
4051
/// Option to set a default GFM code block language if class based language markers are not available
@@ -52,14 +63,17 @@ public enum UnknownTagsOption
5263
/// Include the unknown tag completely into the result. That is, the tag along with the text will be left in output.
5364
/// </summary>
5465
PassThrough,
66+
5567
/// <summary>
5668
/// Drop the unknown tag and its content
5769
/// </summary>
5870
Drop,
71+
5972
/// <summary>
6073
/// Ignore the unknown tag but try to convert its content
6174
/// </summary>
6275
Bypass,
76+
6377
/// <summary>
6478
/// Raise an error to let you know
6579
/// </summary>
@@ -72,6 +86,7 @@ public enum TableWithoutHeaderRowHandlingOption
7286
/// By default, first row will be used as header row
7387
/// </summary>
7488
Default,
89+
7590
/// <summary>
7691
/// An empty row will be added as the header row
7792
/// </summary>
@@ -90,10 +105,12 @@ public enum TableWithoutHeaderRowHandlingOption
90105
/// Determines whether url is allowed: WhitelistUriSchemes contains no elements or contains passed url.
91106
/// </summary>
92107
/// <param name="scheme">Scheme name without trailing colon</param>
93-
internal bool IsSchemeWhitelisted(string scheme) {
108+
internal bool IsSchemeWhitelisted(string scheme)
109+
{
94110
if (scheme == null) throw new ArgumentNullException(nameof(scheme));
95-
var isSchemeAllowed = WhitelistUriSchemes == null || WhitelistUriSchemes.Length == 0 || WhitelistUriSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase);
111+
var isSchemeAllowed = WhitelistUriSchemes == null || WhitelistUriSchemes.Length == 0 ||
112+
WhitelistUriSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase);
96113
return isSchemeAllowed;
97114
}
98115
}
99-
}
116+
}

src/ReverseMarkdown/Converter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ public virtual string Convert(string html)
9494
// cleanup multiple new lines
9595
result = Regex.Replace( result, @"(^\p{Zs}*(\r\n|\n)){2,}", Environment.NewLine, RegexOptions.Multiline);
9696

97+
if (Config.SlackFlavored)
98+
{
99+
result = Cleaner.SlackTidy(result);
100+
}
101+
97102
return Config.CleanupUnnecessarySpaces ? result.Trim().FixMultipleNewlines() : result;
98103
}
99104

src/ReverseMarkdown/Converters/Em.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public override string Convert(HtmlNode node)
3030
? " "
3131
: "";
3232

33-
return content.EmphasizeContentWhitespaceGuard("*", spaceSuffix);
33+
var emphasis = Converter.Config.SlackFlavored ? "_" : "*";
34+
return content.EmphasizeContentWhitespaceGuard(emphasis, spaceSuffix);
3435
}
3536

3637
private static bool AlreadyItalic(HtmlNode node)

src/ReverseMarkdown/Converters/Hr.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ public Hr(Converter converter) : base(converter)
1212

1313
public override string Convert(HtmlNode node)
1414
{
15+
if (Converter.Config.SlackFlavored)
16+
{
17+
throw new SlackUnsupportedTagException(node.Name);
18+
}
19+
1520
return $"{Environment.NewLine}* * *{Environment.NewLine}";
1621
}
1722
}

0 commit comments

Comments
 (0)