Skip to content

Commit 6a46b1b

Browse files
committed
#25 Replace NodePlainTextFormatter with PlainTextNodeRenderer.
The previously introduced NodePlainTextFormatter is flawed. The task of tweaking plain-text conversion behavior while keeping the rest of the existing behavior unchanged was non-trivial.
1 parent afcbfe4 commit 6a46b1b

File tree

7 files changed

+342
-230
lines changed

7 files changed

+342
-230
lines changed

MwParserFromScratch/Nodes/Inline.cs

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
using System.Linq;
66
using System.Net;
77
using System.Text;
8-
using System.Text.RegularExpressions;
9-
using System.Threading.Tasks;
8+
using MwParserFromScratch.Rendering;
109

1110
namespace MwParserFromScratch.Nodes;
1211

@@ -46,13 +45,11 @@ public override string ToString()
4645
return Content;
4746
}
4847

49-
/// <param name="builder"></param>
50-
/// <param name="formatter"></param>
5148
/// <inheritdoc />
52-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
49+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
5350
{
5451
// Unescape HTML entities.
55-
builder.Append(WebUtility.HtmlDecode(Content));
52+
renderer.OutputBuilder.Append(WebUtility.HtmlDecode(Content));
5653
}
5754
}
5855

@@ -97,29 +94,28 @@ protected override Node CloneCore()
9794
/// <inheritdoc />
9895
public override string ToString() => Text == null ? $"[[{Target}]]" : $"[[{Target}|{Text}]]";
9996

100-
/// <param name="builder"></param>
101-
/// <param name="formatter"></param>
10297
/// <inheritdoc />
103-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
98+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
10499
{
105100
// Target == null when parsing `[[]]` with AllowEmptyWikiLinkTarget enabled.
106101
if (Text == null)
107102
{
108103
if (Target != null)
109104
{
110-
formatter(Target, builder);
105+
renderer.RenderNode(Target);
111106
}
112107
return;
113108
}
114109
if (Text.Inlines.Count > 0)
115110
{
116-
formatter(Text, builder);
111+
renderer.RenderNode(Text);
117112
return;
118113
}
119114
// Pipe trick. E.g.
120115
// [[abc (disambiguation)|]] --> [[abc (disambiguation)|abc]]
116+
var builder = renderer.OutputBuilder;
121117
var pos1 = builder.Length;
122-
formatter(Target, builder);
118+
renderer.RenderNode(Target);
123119
if (builder.Length - pos1 >= 3 && builder[builder.Length - 1] == ')')
124120
{
125121
for (var pos2 = pos1 + 1; pos2 < builder.Length - 1; pos2++)
@@ -201,15 +197,15 @@ public override string ToString()
201197
}
202198

203199
/// <inheritdoc />
204-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
200+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
205201
{
206202
var alt = Arguments.Alt;
207-
if (alt != null) formatter(alt, builder);
203+
if (alt != null) renderer.RenderNode(alt);
208204
var caption = Arguments.Caption;
209205
// delimit alt text and caption with a space.
210206
if (alt != null && caption != null)
211-
builder.Append(' ');
212-
if (caption != null) formatter(caption, builder);
207+
renderer.OutputBuilder.Append(' ');
208+
if (caption != null) renderer.RenderNode(caption);
213209
}
214210
}
215211

@@ -276,9 +272,7 @@ public override string ToString()
276272
/// <summary>
277273
/// Infrastructure. This function will always throw a <seealso cref="NotSupportedException"/>.
278274
/// </summary>
279-
/// <param name="builder"></param>
280-
/// <param name="formatter"></param>
281-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
275+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
282276
{
283277
throw new NotSupportedException();
284278
}
@@ -340,21 +334,20 @@ public override string ToString()
340334
return s;
341335
}
342336

343-
/// <param name="builder"></param>
344-
/// <param name="formatter"></param>
345337
/// <inheritdoc />
346-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
338+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
347339
{
348340
if (!Brackets)
349341
{
350-
formatter(Target, builder);
342+
renderer.RenderNode(Target);
351343
}
352344
else
353345
{
346+
var builder = renderer.OutputBuilder;
354347
if (Text != null)
355348
{
356349
var pos1 = builder.Length;
357-
formatter(Text, builder);
350+
renderer.RenderNode(Text);
358351
for (var i = pos1; i < builder.Length; i++)
359352
if (!char.IsWhiteSpace(builder[i]))
360353
return;
@@ -550,12 +543,11 @@ public override string ToString()
550543
return Name + "=" + Value;
551544
}
552545

546+
/// <inheritdoc />
553547
/// <summary>
554548
/// Infrastructure. This function will always throw a <seealso cref="NotSupportedException"/>.
555549
/// </summary>
556-
/// <param name="builder"></param>
557-
/// <param name="formatter"></param>
558-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
550+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
559551
{
560552
throw new NotSupportedException();
561553
}
@@ -824,10 +816,10 @@ public override TagStyle TagStyle
824816
};
825817

826818
/// <inheritdoc />
827-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
819+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
828820
{
829821
if (Name != null && plainTextInvisibleTags.Contains(Name)) return;
830-
builder.Append(Content);
822+
renderer.OutputBuilder.Append(Content);
831823
}
832824
}
833825

@@ -902,7 +894,7 @@ public override TagStyle TagStyle
902894
protected override void BuildContentString(StringBuilder builder) => builder.Append(Content);
903895

904896
/// <inheritdoc />
905-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
897+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
906898
{
907899
if (string.Equals(Name, "br", StringComparison.OrdinalIgnoreCase)
908900
|| string.Equals(Name, "hr", StringComparison.OrdinalIgnoreCase))
@@ -913,11 +905,11 @@ internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextForma
913905
// <br></br> will be rendered as <br /><br />.
914906
// In practice, however, <br>abc</br> won't be parsed correctly, as <br> itself as self-closing.
915907
// See WikitextParserOptions.DefaultSelfClosingOnlyTags
916-
builder.Append('\n');
908+
renderer.OutputBuilder.Append('\n');
917909
if (Content != null)
918910
{
919-
formatter(Content, builder);
920-
builder.Append('\n');
911+
renderer.RenderNode(Content);
912+
renderer.OutputBuilder.Append('\n');
921913
}
922914
return;
923915
}
@@ -927,7 +919,7 @@ internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextForma
927919
// could be otherwise overridden into block-style.
928920
// We leave such triaging responsibility to the library consumer (`formatter` arg).
929921

930-
if (Content != null) formatter(Content, builder);
922+
if (Content != null) renderer.RenderNode(Content);
931923
}
932924

933925
}
@@ -1068,10 +1060,8 @@ public override string ToString()
10681060
+ WhitespaceAfterEqualSign + quote + Value + quote;
10691061
}
10701062

1071-
/// <param name="builder"></param>
1072-
/// <param name="formatter"></param>
10731063
/// <inheritdoc />
1074-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
1064+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
10751065
{
10761066
throw new NotSupportedException();
10771067
}

MwParserFromScratch/Nodes/Node.cs

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
using System.Linq;
55
using System.Reflection;
66
using System.Text;
7-
using System.Threading.Tasks;
7+
using System.Threading;
88
using System.Xml.Linq;
9+
using MwParserFromScratch.Rendering;
910

1011
namespace MwParserFromScratch.Nodes;
1112

@@ -362,55 +363,50 @@ public Node Clone()
362363
return newInst;
363364
}
364365

365-
private static void DefaultNodePlainTextFormatter(Node node, StringBuilder builder)
366-
{
367-
Debug.Assert(node != null);
368-
Debug.Assert(builder != null);
369-
node.ToPlainTextCore(builder, DefaultNodePlainTextFormatter);
370-
}
366+
private static PlainTextNodeRenderer defaultRendererInstCache;
371367

372-
/// <inheritdoc cref="ToPlainText(NodePlainTextFormatter)"/>
368+
/// <inheritdoc cref="ToPlainText(PlainTextNodeRenderer)"/>
373369
public string ToPlainText()
374370
{
375-
return ToPlainText((NodePlainTextFormatter) null);
371+
return ToPlainText(null);
376372
}
377373

378374
/// <summary>
379375
/// Gets the plain text without the unprintable nodes (e.g. comments, templates), with customized formatter.
380376
/// </summary>
381-
/// <param name="formatter">The formatter delegate used to format the <strong>child</strong> nodes, or <c>null</c> to use default formatter.</param>
382-
public string ToPlainText(NodePlainTextFormatter formatter)
377+
/// <param name="renderer">The formatter delegate used to format the <strong>child</strong> nodes, or <c>null</c> to use default formatter.</param>
378+
public string ToPlainText(PlainTextNodeRenderer renderer)
383379
{
384380
var sb = new StringBuilder();
385-
ToPlainText(sb, formatter);
386-
return sb.ToString();
387-
}
388381

389-
/// <inheritdoc cref="ToPlainTextCore"/>
390-
public void ToPlainText(StringBuilder builder)
391-
{
392-
ToPlainText(builder, null);
382+
if (renderer == null)
383+
{
384+
var render = Interlocked.Exchange(ref defaultRendererInstCache, null) ?? new PlainTextNodeRenderer();
385+
try
386+
{
387+
render.RenderNode(sb, this);
388+
return sb.ToString();
389+
}
390+
finally
391+
{
392+
Interlocked.CompareExchange(ref defaultRendererInstCache, render, null);
393+
}
394+
}
395+
396+
renderer.RenderNode(sb, this);
397+
return sb.ToString();
393398
}
394399

395400
/// <summary>
396-
/// Gets the plain text without the unprintable nodes (e.g. comments, templates), with customized formatter.
401+
/// Provides the default implementation for <see cref="PlainTextNodeRenderer.RenderNode(Node)"/>.
397402
/// </summary>
398-
/// <param name="builder">The <see cref="StringBuilder"/> used to receive the content.</param>
399-
/// <param name="formatter">The formatter delegate used to format the <strong>child</strong> nodes, or <c>null</c> to use default formatter.</param>
400-
public void ToPlainText(StringBuilder builder, NodePlainTextFormatter formatter)
401-
{
402-
if (builder == null) throw new ArgumentNullException(nameof(builder));
403-
ToPlainTextCore(builder, formatter ?? DefaultNodePlainTextFormatter);
404-
}
405-
406-
internal virtual void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
403+
internal virtual void RenderAsPlainText(PlainTextNodeRenderer renderer)
407404
{
408405
// The default implementation is to write nothing.
409-
Debug.Assert(builder != null);
410-
Debug.Assert(formatter != null);
406+
Debug.Assert(renderer != null);
411407
}
412408

413-
private class LineInfoAnnotation
409+
private sealed class LineInfoAnnotation
414410
{
415411
internal readonly int StartLineNumber;
416412
internal readonly int StartLinePosition;
@@ -426,19 +422,9 @@ public LineInfoAnnotation(int startLineNumber, int startLinePosition, int endLin
426422
}
427423
}
428424

429-
private class ExtraParsingAnnotation
425+
private sealed class ExtraParsingAnnotation
430426
{
431427
internal bool InferredClosingMark = false;
432428
}
433429

434430
}
435-
436-
/// <summary>
437-
/// Formats the specified <see cref="Node"/> into <c>string</c>.
438-
/// </summary>
439-
/// <param name="node">The node to be formatted.</param>
440-
/// <param name="builder"><see cref="StringBuilder"/> the formatted plaintext should be appended into.</param>
441-
/// <remarks>
442-
/// <param>A default <see cref="NodePlainTextFormatter"/> implementation is to call <c>node.ToPlainText(builder)</c> directly.</param>
443-
/// </remarks>
444-
public delegate void NodePlainTextFormatter(Node node, StringBuilder builder);

MwParserFromScratch/Nodes/Table.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Diagnostics;
44
using System.Linq;
55
using System.Text;
6+
using MwParserFromScratch.Rendering;
67

78
namespace MwParserFromScratch.Nodes;
89

@@ -60,18 +61,17 @@ public override string ToString()
6061
}
6162

6263
/// <inheritdoc />
63-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
64+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
6465
{
65-
if (Caption != null)
66-
formatter(_Caption, builder);
66+
if (_Caption != null) renderer.RenderNode(_Caption);
6767
var firstRow = true;
6868
foreach (var r in Rows)
6969
{
7070
if (firstRow)
7171
firstRow = false;
7272
else
73-
builder.AppendLine();
74-
formatter(r, builder);
73+
renderer.OutputBuilder.AppendLine();
74+
renderer.RenderNode(r);
7575
}
7676
}
7777
}
@@ -116,9 +116,9 @@ public override IEnumerable<Node> EnumChildren()
116116
}
117117

118118
/// <inheritdoc />
119-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
119+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
120120
{
121-
if (Content != null) formatter(Content, builder);
121+
if (Content != null) renderer.RenderNode(Content);
122122
}
123123
}
124124

@@ -200,16 +200,16 @@ public override string ToString()
200200
}
201201

202202
/// <inheritdoc />
203-
internal override void ToPlainTextCore(StringBuilder builder, NodePlainTextFormatter formatter)
203+
internal override void RenderAsPlainText(PlainTextNodeRenderer renderer)
204204
{
205205
var isFirst = true;
206206
foreach (var cell in Cells)
207207
{
208208
if (isFirst)
209209
isFirst = false;
210210
else
211-
builder.Append('\t');
212-
formatter(cell, builder);
211+
renderer.OutputBuilder.Append('\t');
212+
renderer.RenderNode(cell);
213213
}
214214
}
215215
}

0 commit comments

Comments
 (0)