Skip to content

Commit fc70523

Browse files
committed
Fix emoji table alignment regression (#943)
1 parent 25506f2 commit fc70523

4 files changed

Lines changed: 210 additions & 5 deletions

File tree

src/Markdig.Tests/TestCustomEmojis.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,54 @@ public void TestEmojiInPipeTable(string input, string expected)
137137
var actual = Markdown.ToHtml(input, pipeline);
138138
Assert.AreEqual(expected, actual);
139139
}
140-
}
140+
141+
[Test]
142+
[TestCase(true)]
143+
[TestCase(false)]
144+
public void TestEmojiDoesNotBreakPipeTableAlignment(bool useEmojiFirst)
145+
{
146+
const string input = "| Left | Center | Right |\n| ---- |:------:| -----:|\n| a | b | c |";
147+
const string expected = "<table>\n<thead>\n<tr>\n<th>Left</th>\n<th style=\"text-align: center;\">Center</th>\n<th style=\"text-align: right;\">Right</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>a</td>\n<td style=\"text-align: center;\">b</td>\n<td style=\"text-align: right;\">c</td>\n</tr>\n</tbody>\n</table>\n";
148+
149+
var pipelineBuilder = new MarkdownPipelineBuilder();
150+
if (useEmojiFirst)
151+
{
152+
pipelineBuilder.UseEmojiAndSmiley().UsePipeTables();
153+
}
154+
else
155+
{
156+
pipelineBuilder.UsePipeTables().UseEmojiAndSmiley();
157+
}
158+
159+
var pipeline = pipelineBuilder.Build();
160+
var actual = Markdown.ToHtml(input, pipeline);
161+
Assert.AreEqual(expected, actual);
162+
}
163+
164+
[Test]
165+
public void TestEmojiDoesNotBreakGridTableAlignment()
166+
{
167+
const string input = "+-----+:---:+-----+\n| A | :x: | C |\n+-----+-----+-----+";
168+
const string expected = "<table>\n<col style=\"width:33.33%\" />\n<col style=\"width:33.33%\" />\n<col style=\"width:33.33%\" />\n<tbody>\n<tr>\n<td>A</td>\n<td style=\"text-align: center;\">❌</td>\n<td>C</td>\n</tr>\n</tbody>\n</table>\n";
169+
170+
var pipeline = new MarkdownPipelineBuilder()
171+
.UseGridTables()
172+
.UseEmojiAndSmiley()
173+
.Build();
174+
175+
var actual = Markdown.ToHtml(input, pipeline);
176+
Assert.AreEqual(expected, actual);
177+
}
178+
179+
[Test]
180+
public void TestPipeTableExtensionDoesNotSuppressNeutralFaceInParagraph()
181+
{
182+
var pipeline = new MarkdownPipelineBuilder()
183+
.UsePipeTables()
184+
.UseEmojiAndSmiley()
185+
.Build();
186+
187+
var actual = Markdown.ToHtml("text :|", pipeline);
188+
Assert.AreEqual("<p>text 😐</p>\n", actual);
189+
}
190+
}

src/Markdig/Extensions/Emoji/EmojiExtension.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// This file is licensed under the BSD-Clause 2 license.
33
// See the license.txt file in the project root for more information.
44

5+
using Markdig.Parsers.Inlines;
56
using Markdig.Renderers;
67

78
namespace Markdig.Extensions.Emoji;
@@ -32,8 +33,9 @@ public void Setup(MarkdownPipelineBuilder pipeline)
3233
{
3334
if (!pipeline.InlineParsers.Contains<EmojiParser>())
3435
{
35-
// Insert the parser before any other parsers
36-
pipeline.InlineParsers.Insert(0, new EmojiParser(EmojiMapping));
36+
// Insert before emphasis so emoji shortcodes are parsed early, while preserving
37+
// precedence for structural parsers registered earlier in the pipeline.
38+
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(new EmojiParser(EmojiMapping));
3739
}
3840
}
3941

src/Markdig/Extensions/Tables/PipeTableExtension.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// This file is licensed under the BSD-Clause 2 license.
33
// See the license.txt file in the project root for more information.
44

5+
using Markdig.Extensions.Emoji;
56
using Markdig.Parsers.Inlines;
67
using Markdig.Renderers;
78

@@ -46,7 +47,11 @@ public void Setup(MarkdownPipelineBuilder pipeline)
4647
// delimiter inside a cell, e.g. the subscript `~` in `**~$1.50**`, can leave pipe delimiters nested
4748
// in an emphasis delimiter tree and make the table parser see phantom cells. PipeTableParser re-runs
4849
// inline post-processors inside each extracted cell after the table structure has been fixed.
49-
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(new PipeTableParser(lineBreakParser!, Options));
50+
var pipeTableParser = new PipeTableParser(lineBreakParser!, Options);
51+
if (!pipeline.InlineParsers.InsertBefore<EmojiParser>(pipeTableParser))
52+
{
53+
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(pipeTableParser);
54+
}
5055
}
5156
}
5257

src/Markdig/Extensions/Tables/PipeTableParser.cs

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
2929
public PipeTableParser(LineBreakInlineParser lineBreakParser, PipeTableOptions? options = null)
3030
{
3131
_lineBreakParser = lineBreakParser ?? throw new ArgumentNullException(nameof(lineBreakParser));
32-
OpeningCharacters = ['|', '\n', '\r'];
32+
OpeningCharacters = ['|', '\n', '\r', ':'];
3333
Options = options ?? new PipeTableOptions();
3434
}
3535

@@ -51,6 +51,18 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
5151

5252
var c = slice.CurrentChar;
5353

54+
if (c == ':')
55+
{
56+
var tableStateForColon = processor.ParserStates[Index] as TableState;
57+
if (tableStateForColon is null || !IsHeaderSeparatorColonBeforePipe(slice))
58+
{
59+
return false;
60+
}
61+
62+
MatchLiteralColon(processor, ref slice);
63+
return true;
64+
}
65+
5466
// If we have not a delimiter on the first line of a paragraph, don't bother to continue
5567
// tracking other delimiters on following lines
5668
var tableState = processor.ParserStates[Index] as TableState;
@@ -116,6 +128,142 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
116128
return true;
117129
}
118130

131+
private static bool IsHeaderSeparatorColonBeforePipe(StringSlice slice)
132+
{
133+
if (slice.PeekChar() != '|')
134+
{
135+
return false;
136+
}
137+
138+
var text = slice.Text;
139+
int currentLineStart = FindLineStart(text, slice.Start);
140+
int currentLineEnd = FindLineEnd(text, slice.Start, slice.End);
141+
142+
if (!IsHeaderSeparatorLine(text, currentLineStart, currentLineEnd))
143+
{
144+
return false;
145+
}
146+
147+
int previousLineStart = FindPreviousLineStart(text, currentLineStart);
148+
return previousLineStart >= 0 && LineHasPipe(text, previousLineStart, currentLineStart - 1);
149+
}
150+
151+
private static void MatchLiteralColon(InlineProcessor processor, ref StringSlice slice)
152+
{
153+
int position = processor.GetSourcePosition(slice.Start, out int line, out int column);
154+
if (processor.Inline is LiteralInline previousLiteral
155+
&& ReferenceEquals(previousLiteral.Content.Text, slice.Text)
156+
&& previousLiteral.Content.End + 1 == slice.Start)
157+
{
158+
previousLiteral.Content.End = slice.Start;
159+
previousLiteral.Span.End = position;
160+
processor.Inline = previousLiteral;
161+
}
162+
else
163+
{
164+
processor.Inline = new LiteralInline
165+
{
166+
Content = new StringSlice(slice.Text, slice.Start, slice.Start),
167+
Span = new SourceSpan(position, position),
168+
Line = line,
169+
Column = column,
170+
};
171+
}
172+
173+
slice.SkipChar();
174+
}
175+
176+
private static int FindLineStart(string text, int position)
177+
{
178+
for (int i = position - 1; i >= 0; i--)
179+
{
180+
if (text[i] == '\r' || text[i] == '\n')
181+
{
182+
return i + 1;
183+
}
184+
}
185+
186+
return 0;
187+
}
188+
189+
private static int FindLineEnd(string text, int position, int sliceEnd)
190+
{
191+
int end = Math.Min(sliceEnd, text.Length - 1);
192+
for (int i = position; i <= end; i++)
193+
{
194+
if (text[i] == '\r' || text[i] == '\n')
195+
{
196+
return i - 1;
197+
}
198+
}
199+
200+
return end;
201+
}
202+
203+
private static int FindPreviousLineStart(string text, int currentLineStart)
204+
{
205+
int previousLineEnd = currentLineStart - 2;
206+
if (previousLineEnd >= 0 && text[previousLineEnd] == '\r')
207+
{
208+
previousLineEnd--;
209+
}
210+
211+
if (previousLineEnd < 0)
212+
{
213+
return -1;
214+
}
215+
216+
for (int i = previousLineEnd; i >= 0; i--)
217+
{
218+
if (text[i] == '\r' || text[i] == '\n')
219+
{
220+
return i + 1;
221+
}
222+
}
223+
224+
return 0;
225+
}
226+
227+
private static bool IsHeaderSeparatorLine(string text, int start, int end)
228+
{
229+
bool hasPipe = false;
230+
bool hasDash = false;
231+
232+
for (int i = start; i <= end; i++)
233+
{
234+
switch (text[i])
235+
{
236+
case '|':
237+
hasPipe = true;
238+
break;
239+
case '-':
240+
hasDash = true;
241+
break;
242+
case ':':
243+
case ' ':
244+
case '\t':
245+
break;
246+
default:
247+
return false;
248+
}
249+
}
250+
251+
return hasPipe && hasDash;
252+
}
253+
254+
private static bool LineHasPipe(string text, int start, int end)
255+
{
256+
for (int i = start; i <= end; i++)
257+
{
258+
if (text[i] == '|')
259+
{
260+
return true;
261+
}
262+
}
263+
264+
return false;
265+
}
266+
119267
/// <summary>
120268
/// Performs the post process operation.
121269
/// </summary>

0 commit comments

Comments
 (0)