Skip to content

Commit 4907eb3

Browse files
committed
Replacement markup parsing changes.
Fixed a bug where self-closing replacement markup was consuming whitespace. Changed how invisible characters work in replacement markup, now you can say how many (if any) invisible characters were added into the string during replacement.
1 parent aa22bf0 commit 4907eb3

File tree

4 files changed

+180
-26
lines changed

4 files changed

+180
-26
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

99
### Added
1010

11+
- `ReplacementMarkerResult` struct that encapsulates diagnostics of replacement markup processors and also an invisible characters count.
12+
- this allows for replacement markup to declare how many invisible characters they have added
13+
- this is necessary because both Unity and Godot (and likely every tool out there) has some variant of rich text strings where the attributes on the rich text is within the string itself
14+
- any sibling markup following the replacement needs to not be pushed down by the invisible elements of the string
15+
1116
### Updated
1217

1318
- When the `tags:` header on a node is blank, `Node.Tags` now returns an empty collection, instead of a collection containing a single empty string.
@@ -22,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2227
- Language Server: hovering smart variables now shows their definition.
2328
- Language Server: Make compilation asynchronous
2429
- Language Server: 'Undeclared variable' warnings have had their severity reduced to "hint".
30+
- Fixed a bug where self-closing replacement markup was consuming whitespace
31+
- `IAttributeMarkerProcessor` now return a `ReplacementMarkerResult` struct instead of just a list of diagnostics
2532

2633
### Removed
2734

YarnSpinner.Tests/MarkupTests.cs

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,39 @@ void TestSquishedMarkupStringsWithRewritersAreValid(string line, string comparis
694694
attributes.Should().HaveCount(attributeCount);
695695
}
696696

697-
List<LineParser.MarkupDiagnostic> IAttributeMarkerProcessor.ProcessReplacementMarker(MarkupAttribute marker, StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode)
697+
[Theory]
698+
[InlineData("this is a line with non-replacement[a/] markup", "this is a line with non-replacement markup", new string[] { "a" },new int[] { 35 })]
699+
[InlineData("this is a line [bold]with some replacement[/bold] markup and a non-replacement[a/] markup", "this is a line <b>with some replacement</b> markup and a non-replacement markup", new string[] { "a" }, new int[] { 65 })]
700+
[InlineData("this is a [bold]line with some [italics]nested[a trimwhitespace=false /] tags[/italics][b trimwhitespace=false /][/bold] in[c trimwhitespace=false /] it", "this is a <b>line with some <i>nested tags</i></b> in it", new string[] { "a", "b", "c" }, new int[] { 31, 36, 39 })]
701+
[InlineData("this is a line with [blocky]markup[/blocky] that actually has[a trimwhitespace=false /] visible characters", "this is a line with [markup] that actually has visible characters", new string[] { "a" }, new int[] { 46 })]
702+
[InlineData("this is a line with [wacky]markup[/wacky] that actually has[a trimwhitespace=false /] both", "this is a line with <b>[markup]</b> that actually has both", new string[] { "a" }, new int[] { 46 })]
703+
[InlineData("this is a line with [wacky]internal[a trimwhitespace=false /] both[/wacky] markup","this is a line with <b>[internal both]</b> markup", new string[] { "a" }, new int[] { 29 })]
704+
[InlineData("this is a [wacky]line with some [blocky]nested[a trimwhitespace=false /] tags[/blocky][b trimwhitespace=false /][/wacky] in[c trimwhitespace=false /] it", "this is a <b>[line with some [nested tags]]</b> in it", new string[] { "a", "b", "c" }, new int[] { 33, 39, 43 })]
705+
void TestSquishedMarkupStringsWithInvisibleCharactersAreValid(string line, string comparison, string[] markerNames, int[] markerPositions)
706+
{
707+
markerNames.Should().HaveSameCount(markerPositions);
708+
709+
var lineParser = new LineParser();
710+
lineParser.RegisterMarkerProcessor("bold", this);
711+
lineParser.RegisterMarkerProcessor("italics", this);
712+
lineParser.RegisterMarkerProcessor("blocky", this);
713+
lineParser.RegisterMarkerProcessor("wacky", this);
714+
715+
var result = lineParser.ParseStringWithDiagnostics(line, "en");
716+
717+
result.diagnostics.Should().BeEmpty();
718+
result.markup.Text.Should().Be(comparison);
719+
result.markup.Attributes.Should().HaveCount(markerPositions.Length);
720+
721+
for (int i = 0; i < markerNames.Length; i++)
722+
{
723+
var name = markerNames[i];
724+
result.markup.TryGetAttributeWithName(name, out var attribute).Should().BeTrue();
725+
attribute.Position.Should().Be(markerPositions[i]);
726+
}
727+
}
728+
729+
public ReplacementMarkerResult ProcessReplacementMarker(MarkupAttribute marker, StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode)
698730
{
699731
var diagnostics = new List<LineParser.MarkupDiagnostic>();
700732
switch (marker.Name)
@@ -706,7 +738,37 @@ void TestSquishedMarkupStringsWithRewritersAreValid(string line, string comparis
706738
childBuilder.Insert(0, "<b>");
707739
childBuilder.Append("</b>");
708740

709-
return diagnostics;
741+
return new ReplacementMarkerResult(diagnostics, 7);
742+
}
743+
case "italics":
744+
{
745+
childBuilder.Insert(0, "<i>");
746+
childBuilder.Append("</i>");
747+
return new ReplacementMarkerResult(diagnostics, 7);
748+
}
749+
case "blocky":
750+
{
751+
childBuilder.Insert(0, '[');
752+
childBuilder.Append(']');
753+
754+
for (int i = 0; i < childAttributes.Count; i++)
755+
{
756+
childAttributes[i] = childAttributes[i].Shift(1);
757+
}
758+
759+
return new ReplacementMarkerResult(diagnostics, 0);
760+
}
761+
case "wacky":
762+
{
763+
childBuilder.Insert(0, "<b>[");
764+
childBuilder.Append("]</b>");
765+
766+
for (int i = 0; i < childAttributes.Count; i++)
767+
{
768+
childAttributes[i] = childAttributes[i].Shift(1);
769+
}
770+
771+
return new ReplacementMarkerResult(diagnostics, 7);
710772
}
711773
case "localise":
712774
{
@@ -719,16 +781,22 @@ void TestSquishedMarkupStringsWithRewritersAreValid(string line, string comparis
719781
{
720782
childBuilder.Append("chat");
721783
}
722-
return diagnostics;
784+
return new ReplacementMarkerResult(diagnostics, 0);
785+
}
786+
case "scr":
787+
{
788+
childBuilder.Append("scr");
789+
return new ReplacementMarkerResult(diagnostics, 0);
723790
}
724791
default:
725792
{
726793
childBuilder.Append("Unrecognised markup name: ");
727794
childBuilder.Append(marker.Name);
728-
return diagnostics;
795+
return new ReplacementMarkerResult(diagnostics, 0);
729796
}
730797
}
731798
}
799+
732800
[Fact]
733801
void TestLocalisedStringReplacement()
734802
{
@@ -1294,6 +1362,20 @@ public void TestOlderSiblingNearReplacementMarkersCorrectlyRespectsWhitespaceCon
12941362

12951363
markup.markup.Text.Should().Be(expected);
12961364
}
1365+
1366+
[Theory]
1367+
[InlineData("a line with a self-closing[scr /] replacement tag","a line with a self-closingscr replacement tag")]
1368+
[InlineData("a line with a self-closing[scnr /] -non-replacement tag","a line with a self-closing-non-replacement tag")]
1369+
[InlineData("a line with a self-closing[scnr trimwhitespace=false /] non-replacement tag","a line with a self-closing non-replacement tag")]
1370+
public void TestSelfclosingReplacementMarkersDoNotConsumeWhitespace(string line, string expected)
1371+
{
1372+
var lineParser = new LineParser();
1373+
lineParser.RegisterMarkerProcessor("scr", this);
1374+
1375+
var markup = lineParser.ParseStringWithDiagnostics(line, "en-AU");
1376+
markup.diagnostics.Should().BeEmpty();
1377+
markup.markup.Text.Should().Be(expected);
1378+
}
12971379
}
12981380

12991381
public class BBCodeChevronReplacer : IAttributeMarkerProcessor
@@ -1304,18 +1386,18 @@ public BBCodeChevronReplacer(string markerName)
13041386
tag = markerName;
13051387
}
13061388

1307-
public List<LineParser.MarkupDiagnostic> ProcessReplacementMarker(MarkupAttribute marker, StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode)
1389+
public ReplacementMarkerResult ProcessReplacementMarker(MarkupAttribute marker, StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode)
13081390
{
13091391
List<LineParser.MarkupDiagnostic> diagnostics = new List<LineParser.MarkupDiagnostic>();
13101392
if (marker.Name != tag)
13111393
{
13121394
diagnostics.Add(new LineParser.MarkupDiagnostic($"Asked to replace {marker.Name} but this only handles {tag} markers."));
1313-
return diagnostics;
1395+
return new ReplacementMarkerResult(diagnostics, 0);
13141396
}
13151397

13161398
childBuilder.Insert(0, $"<{tag}>");
13171399
childBuilder.Append($"</{tag}>");
1318-
return diagnostics;
1400+
return new ReplacementMarkerResult(diagnostics, 5 + tag.Length * 2);
13191401
}
13201402
}
13211403
}

YarnSpinner/YarnSpinner.Markup/IAttributeMarkerProcessor.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,58 @@ public interface IAttributeMarkerProcessor
6262
/// </list>
6363
/// </example>
6464
/// <returns>The collection of diagnostics produced during processing,
65-
/// if any.</returns>
66-
public List<LineParser.MarkupDiagnostic> ProcessReplacementMarker(MarkupAttribute marker, System.Text.StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode);
65+
/// and the number of invisible characters created during processing.</returns>
66+
public ReplacementMarkerResult ProcessReplacementMarker(MarkupAttribute marker, System.Text.StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode);
67+
}
68+
69+
/// <summary>
70+
/// A bundle of results from processing replacement markers.
71+
/// </summary>
72+
/// <seealso cref="IAttributeMarkerProcessor"/>
73+
public struct ReplacementMarkerResult
74+
{
75+
/// <summary>
76+
/// The collection of diagnostics produced during processing, if any.
77+
/// </summary>
78+
public List<LineParser.MarkupDiagnostic> Diagnostics;
79+
/// <summary>
80+
/// The number of invisible characters added into the line during processing.
81+
/// </summary>
82+
/// <remarks>
83+
/// This will vary depending on what the replacement markup needs to do.
84+
/// <list type="bullet">
85+
/// <item>
86+
/// <para>
87+
/// When only inserting rich-text tags, this should be the length of all inserted text.
88+
/// For example <c>"this is text with [bold]some bold[/bold] elements"</c> translated into Unity style rich-text become <c>"this is text with &lt;b>some bold&lt;/b> elements"</c>.
89+
/// In this case then the value of <c>InvisibleCharacters</c> would be seven.
90+
/// </para>
91+
/// </item>
92+
/// <item>
93+
/// <para>
94+
/// When only modifying the content of the children text, such as making all text upper case, this should be <c>0</c>.
95+
/// For example <c>"this is text with [upper]some uppercased[/upper] elements"</c> is transformed into <c>"this is text with SOME UPPERCASED elements"</c>.
96+
/// The number of invisible character will be zero.
97+
/// </para>
98+
/// </item>
99+
/// <item>
100+
/// When adding new content into the line (regardless of being added at the start, end, or middle) this should be zero but the replacement processor should make sure to shift along it's children attributes where appropriate.
101+
/// For example <c>"this is text with [emph]some emphasised[/emph] elements"</c> transformed into <c>"this is text with !!some emphasised!! elements"</c> the value of <c>InvisibleCharacters</c> would be zero.
102+
/// In this case however the <c>childAttributes</c> in <see cref="IAttributeMarkerProcessor.ProcessReplacementMarker"/> would need to be shifted down two.
103+
/// </item>
104+
/// </list>
105+
/// </remarks>
106+
public int InvisibleCharacters;
107+
108+
/// <summary>
109+
/// Convenience constructor for replacement markup results.
110+
/// </summary>
111+
/// <param name="diagnostics">The diagnostics generated during processing</param>
112+
/// <param name="invisibleCharacters">the number of invisible characters generated during processing.</param>
113+
public ReplacementMarkerResult(List<LineParser.MarkupDiagnostic> diagnostics, int invisibleCharacters)
114+
{
115+
this.Diagnostics = diagnostics;
116+
this.InvisibleCharacters = invisibleCharacters;
117+
}
67118
}
68119
}

0 commit comments

Comments
 (0)