Skip to content

Commit 221ee24

Browse files
committed
Allow marker processors to work with the implicit character attribute
1 parent 4b02a6a commit 221ee24

File tree

3 files changed

+55
-31
lines changed

3 files changed

+55
-31
lines changed

CHANGELOG.md

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

99
### Added
1010

11+
- Marker processors can now process implicit `[character/]` markers.
12+
- Yarn Spinner automatically adds `[character]` markers to lines that begin with a character name. For example, `Mae: Hello!` gets implicitly rewritten to `[character name="Mae"]Mae: [/character]Hello!`. This allows games to know what character is speaking, as well as being able to trim off the character name.
13+
- With this update, marker processors are able to register to handle this implicit marker, allowing games to customise the way that the name is handled.
14+
1115
### Changed
1216

1317
- Fixed a bug where unbalanced markup without text siblings or children would cause an error
1418
- Fixed a bug where numbers inside markup properties were being parsed in a culture variant manner.
19+
- The implicit `[character/]` marker is now generated before all other markup processing takes place. As a result, replacement markers like `[select/]` that produce text that would have been identified as a character name won't have their output recognised as a character name.
20+
- If you need this to keep working, manually add your own `[character name=etc] ... [/character]` markup around your content.
1521

1622
### Removed
1723

YarnSpinner.Tests/MarkupTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,20 @@ public void TestSelfclosingReplacementMarkersDoNotConsumeWhitespace(string line,
15321532
markup.diagnostics.Should().BeEmpty();
15331533
markup.markup.Text.Should().Be(expected);
15341534
}
1535+
1536+
[Fact]
1537+
public void TestMarkerProcessorsCanProcessCharacterNames()
1538+
{
1539+
var lineParser = new LineParser();
1540+
lineParser.RegisterMarkerProcessor("character", new MarkerUppercaseReplacer());
1541+
1542+
var markup = lineParser.ParseString("Mae: I'm talkin' here", "en-AU");
1543+
markup.Text.Should().Be("MAE: I'm talkin' here", "the character marker should be processed");
1544+
markup.Attributes.Should().Contain(m => m.Name == "character", "the marker should be left in place")
1545+
.Which.Properties.Should().Contain(
1546+
kv => kv.Key == "name" && kv.Value.StringValue == "Mae", "the marker's properties should be unmodified"
1547+
);
1548+
}
15351549
}
15361550

15371551
public class BBCodeChevronReplacer : IAttributeMarkerProcessor
@@ -1556,4 +1570,19 @@ public ReplacementMarkerResult ProcessReplacementMarker(MarkupAttribute marker,
15561570
return new ReplacementMarkerResult(diagnostics, 5 + tag.Length * 2);
15571571
}
15581572
}
1573+
1574+
/// <summary>
1575+
/// An <see cref="IAttributeMarkerProcessor"/> that makes markers uppercase.
1576+
/// </summary>
1577+
public class MarkerUppercaseReplacer : IAttributeMarkerProcessor
1578+
{
1579+
public ReplacementMarkerResult ProcessReplacementMarker(MarkupAttribute marker, StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode)
1580+
{
1581+
var contents = childBuilder.ToString();
1582+
childBuilder.Clear();
1583+
childBuilder.Append(contents.ToUpperInvariant());
1584+
childAttributes.Add(marker);
1585+
return new ReplacementMarkerResult([], 0);
1586+
}
1587+
}
15591588
}

YarnSpinner/YarnSpinner.Markup/LineParser.cs

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -777,9 +777,9 @@ private MarkupProperty internalIDproperty()
777777
private MarkupTreeNode? sibling = null;
778778
// this keeps track of all accumulated invisible characters added by any replacement markup.
779779
// this is necessary to prevent a bug with the following:
780-
// "this is a line [bold]with some replacement[/bold] markup and a non-replacement[a/] markup"
780+
// "this is a line [bold]with some replacement[/bold] markup and a non-replacement[a/] markup"
781781
// assuming that this is intended to replace the [bold] with <b> in unity we would end up as though we had written:
782-
// "this is a line <b>with some replacement</b> markup and a non-replacement[a/] markup"
782+
// "this is a line <b>with some replacement</b> markup and a non-replacement[a/] markup"
783783
// but this has now pushed the [a] markup down by 7 invisible characters
784784
// so each replacement markup processor needs to let us know how many (if any) invisible characters there are so we can backshift any sibling attributes after a replacement markup
785785
private int invisibleCharacters = 0;
@@ -1486,8 +1486,11 @@ internal MarkupParseResult ParseString(string input, string localeCode, bool squ
14861486
return ParseStringWithDiagnostics(input, localeCode, squish, sort, addImplicitCharacterAttribute).markup;
14871487
}
14881488

1489-
private static readonly char[] trimChars = { ':', ' ' };
1490-
private static readonly System.Text.RegularExpressions.Regex implicitCharacterRegex = new(@"^[^:]*:\s*");
1489+
private static readonly System.Text.RegularExpressions.Regex implicitCharacterRegex = new(@"^(?<name>[^"":]+)(?<suffix>:\s*)");
1490+
1491+
// Matches a "[character" at the start of the string, which means that
1492+
// the string contains an explicit character marker.
1493+
private static readonly System.Text.RegularExpressions.Regex characterMarkerRegex = new(@"^\s*\[character");
14911494

14921495
internal (MarkupParseResult markup, List<MarkupDiagnostic> diagnostics) ParseStringWithDiagnostics(string input, string localeCode, bool squish = true, bool sort = true, bool addImplicitCharacterAttribute = true)
14931496
{
@@ -1497,6 +1500,19 @@ internal MarkupParseResult ParseString(string input, string localeCode, bool squ
14971500
}
14981501

14991502
input = input.Normalize();
1503+
1504+
if (addImplicitCharacterAttribute && characterMarkerRegex.IsMatch(input) == false)
1505+
{
1506+
// The line does not already contain a [character] marker at the
1507+
// start, and we've been asked to add an implicit character
1508+
// attribute. Attempt to find a character at the start, and
1509+
// replace it with markup that indicates the character name.
1510+
input = implicitCharacterRegex.Replace(
1511+
input,
1512+
(match) => $"[character name=\"{match.Groups["name"]}\"]{match.Groups["name"]}{match.Groups["suffix"]}[/character]"
1513+
);
1514+
}
1515+
15001516
var tokens = LexMarkup(input);
15011517
var parseResult = BuildMarkupTreeFromTokens(tokens, input);
15021518

@@ -1525,33 +1541,6 @@ internal MarkupParseResult ParseString(string input, string localeCode, bool squ
15251541

15261542
var finalText = builder.ToString();
15271543

1528-
if (addImplicitCharacterAttribute)
1529-
{
1530-
var hasCharacterAttributeAlready = false;
1531-
foreach (var attribute in attributes)
1532-
{
1533-
if (attribute.Name == "character")
1534-
{
1535-
hasCharacterAttributeAlready = true;
1536-
break;
1537-
}
1538-
}
1539-
if (!hasCharacterAttributeAlready)
1540-
{
1541-
var match = implicitCharacterRegex.Match(finalText);
1542-
if (match.Success)
1543-
{
1544-
var characterName = match.Value.TrimEnd(trimChars);
1545-
var propertyList = new List<MarkupProperty>
1546-
{
1547-
new MarkupProperty("name", characterName),
1548-
};
1549-
var characterMarker = new MarkupAttribute(0, 0, match.Length, "character", propertyList);
1550-
attributes.Add(characterMarker);
1551-
}
1552-
}
1553-
}
1554-
15551544
if (sort)
15561545
{
15571546
// finally we want them sorted by their position in the source code

0 commit comments

Comments
 (0)